diff --git a/build.gradle b/build.gradle index f14edc54d..b4a156c62 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'eclipse' id 'idea' id 'java-library' - id 'net.neoforged.moddev' version '1.0.0' + id 'net.neoforged.moddev' version '2.0.36-beta' } group = 'com.lovetropics.minigames' @@ -45,9 +45,20 @@ neoForge { addModdingDependenciesTo sourceSets.test + mods { + "${project.mod_id}" { + sourceSet sourceSets.main + } + "${project.mod_id}_test" { + sourceSet sourceSets.main + sourceSet sourceSets.test + } + } + runs { client { client() + loadedMods = [mods."${mod_id}"] programArguments.addAll '--username', 'Dev' + new Random().nextInt(999) systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id @@ -55,6 +66,7 @@ neoForge { server { server() + loadedMods = [mods."${mod_id}"] programArgument '--nogui' systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id @@ -62,24 +74,22 @@ neoForge { gameTestServer { type = "gameTestServer" + sourceSet = sourceSets.test + loadedMods = [mods."${mod_id}_test"] + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } data { data() + loadedMods = [mods."${mod_id}_test"] programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() } configureEach { logLevel = org.slf4j.event.Level.WARN - } - } - - mods { - "${project.mod_id}" { - sourceSet sourceSets.main - sourceSet sourceSets.test + ideName = project.name + " - " + ideName.get() } } } diff --git a/gradle.properties b/gradle.properties index a4dfe180f..3c18ba4f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ neo_version=21.0.167 parchment_version=2024.06.23 registrate_version=1.3.0+50 ltlib_version=[1.4.5,1.5) -ltextras_version=1.3.0-release+7 +ltextras_version=1.3.0-release+15 org.gradle.jvmargs=-Xmx1G org.gradle.daemon=true diff --git a/src/generated/resources/assets/ltminigames/blockstates/trivia_chest.json b/src/generated/resources/assets/ltminigames/blockstates/trivia_chest.json new file mode 100644 index 000000000..111905034 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/blockstates/trivia_chest.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "ltminigames:block/trivia_chest" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/blockstates/trivia_collectable.json b/src/generated/resources/assets/ltminigames/blockstates/trivia_collectable.json new file mode 100644 index 000000000..5a2004237 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/blockstates/trivia_collectable.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "ltminigames:block/trivia_collectable" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/blockstates/trivia_gate.json b/src/generated/resources/assets/ltminigames/blockstates/trivia_gate.json new file mode 100644 index 000000000..83f1f65a8 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/blockstates/trivia_gate.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "ltminigames:block/trivia_gate" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/blockstates/trivia_reward.json b/src/generated/resources/assets/ltminigames/blockstates/trivia_reward.json new file mode 100644 index 000000000..e204dcdbe --- /dev/null +++ b/src/generated/resources/assets/ltminigames/blockstates/trivia_reward.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "ltminigames:block/trivia_reward" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/lang/en_ud.json b/src/generated/resources/assets/ltminigames/lang/en_ud.json index ca57df380..0556271ef 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_ud.json +++ b/src/generated/resources/assets/ltminigames/lang/en_ud.json @@ -15,6 +15,10 @@ "block.ltminigames.plastic_bottle": "ǝןʇʇoᗺ ɔıʇsɐןԀ", "block.ltminigames.plastic_rings": "sbuıᴚ ɔıʇsɐןԀ", "block.ltminigames.straw": "ʍɐɹʇS", + "block.ltminigames.trivia_chest": "ʇsǝɥƆ ɐıʌıɹ⟘", + "block.ltminigames.trivia_collectable": "ǝןqɐʇɔǝןןoƆ ɐıʌıɹ⟘", + "block.ltminigames.trivia_gate": "ǝʇɐ⅁ ɐıʌıɹ⟘", + "block.ltminigames.trivia_reward": "pɹɐʍǝᴚ ɐıʌıɹ⟘", "effect.ltminigames.coin_multiplier_power_up": "dn-ɹǝʍoԀ ɹǝıןdıʇןnW uıoƆ", "effect.ltminigames.knockback_resistance_power_up": "d∩-ɹǝʍoԀ ǝɔuɐʇsısǝᴚ ʞɔɐqʞɔouʞ", "effect.ltminigames.leaky_pockets": "sʇǝʞɔoԀ ʎʞɐǝꞀ", @@ -66,6 +70,8 @@ "ltminigames.command.started_game": "¡%s pǝʇɹɐʇs ǝʌɐɥ noʎ", "ltminigames.command.stopped_game": "¡%s pǝddoʇs ǝʌɐɥ noʎ", "ltminigames.command.team_chat_intro": "˙ǝuoʎɹǝʌǝ ɥʇıʍ ʇɐɥɔ oʇ ןɐqoןb ʇɐɥɔ/ ɹo ʇnoɥs/ ǝs∩ ˙ʇɐɥɔ ɯɐǝʇ buısn ǝɹɐ noʎ", + "ltminigames.container.triviaChest": "ʇsǝɥƆ ɐıʌıɹ⟘", + "ltminigames.container.triviaChestDouble": "ʇsǝɥƆ ɐıʌıɹ⟘ ǝbɹɐꞀ", "ltminigames.minigame.arcade_turtle_race": "ǝɔɐᴚ ǝןʇɹn⟘ ǝpɐɔɹⱯ", "ltminigames.minigame.biodiversity_blitz": "zʇıןᗺ ʎʇısɹǝʌıpoıᗺ", "ltminigames.minigame.biodiversity_blitz.can_only_place_plants": "¡doɥs ǝɥʇ ɯoɹɟ ʇob noʎ sʇuɐןd ǝɔɐןd ʎןuo uɐɔ noʎ", @@ -164,7 +170,14 @@ "ltminigames.minigame.build_competition": "uoıʇıʇǝdɯoƆ pןınᗺ", "ltminigames.minigame.calamity": "ʎʇıɯɐןɐƆ", "ltminigames.minigame.chaos_block_party": "ʎʇɹɐԀ ʞɔoןᗺ soɐɥƆ", + "ltminigames.minigame.connect_four": "ɹnoℲ ʇɔǝuuoƆ", + "ltminigames.minigame.connect_four.teams_goes_next": "ʇxǝu sǝob %s ɯɐǝ⟘", + "ltminigames.minigame.connect_four.your_turn": "ʞɔoןq ɐ ǝɔɐןd oʇ uɹnʇ ɹnoʎ sı ʇI", "ltminigames.minigame.conservation_exploration": "uoıʇɐɹoןdxƎ uoıʇɐʌɹǝsuoƆ", + "ltminigames.minigame.crafting_bee.dont_cheat": "¡ʇɐǝɥɔ ʇ,uoᗡ", + "ltminigames.minigame.crafting_bee.hint": "sʇuǝıpǝɹbuı ɟo ɹǝqɯnu ɯopuɐɹ ɐ ɟo uoıʇısod ǝɥʇ buıʎɐןdsıp 'ʇuıɥ ɐ ʍoɥs oʇ ʞɔıןƆ", + "ltminigames.minigame.crafting_bee.hints_left": "ʇɟǝן sʇuıɥ %s ǝʌɐɥ noʎ", + "ltminigames.minigame.crafting_bee.team_has_completed_recipes": "sǝdıɔǝɹ %s ɟo ʇno %s pǝʇǝןdɯoɔ sɐɥ %s ɯɐǝ⟘", "ltminigames.minigame.donation.acid_rain": "uıɐᴚ pıɔⱯ", "ltminigames.minigame.donation.acid_rain.description": "¡ǝʇnuıɯ Ɩ ɹoɟ spɐǝɥ ,sɹǝʎɐןd ǝɥʇ uo uʍop pıɔɐ uıɐᴚ", "ltminigames.minigame.donation.acid_rain.toast": "¡ǝʇnuıɯ Ɩ ɹoɟ pıɔɐ buıuıɐɹ sı ʇI", @@ -382,6 +395,8 @@ "ltminigames.minigame.results": ":sʇןnsǝɹ ǝɥʇ ǝɹɐ ǝɹǝH ¡ɹǝʌo sı ǝɯɐb ǝɥ⟘", "ltminigames.minigame.reward_item": "%s x%s - ", "ltminigames.minigame.rewards_granted": "¡sǝɯɐbıuıɯ buıʎɐןd ɹoɟ spɹɐʍǝɹ ʇob noʎ", + "ltminigames.minigame.river_race": "ǝɔɐᴚ ɹǝʌıᴚ", + "ltminigames.minigame.river_race.shop": "doɥS", "ltminigames.minigame.signature_run": "unᴚ ǝɹnʇɐubıS", "ltminigames.minigame.spectating_notification": "˙ɯooz oʇ ןןoɹɔs puɐ %s pןoH\n˙sɹǝʎɐןd ʇɔǝןǝs oʇ sʎǝʞ ʍoɹɹɐ ǝɥʇ ǝsn ɹo ןןoɹɔS\n¡%s ɐ ǝɹɐ noʎ", "ltminigames.minigame.spectating_notification.key": "ןoɹʇuoƆ ʇɟǝꞀ", @@ -465,6 +480,7 @@ "ltminigames.minigame.survive_the_tide_intro5": "¡ǝǝs s,ʇǝꞀ\n", "ltminigames.minigame.survive_the_tide_pvp_enabled.subtitle": "˙˙˙sɹǝʎɐןd ɹǝɥʇo ɟo ǝɹɐʍǝᗺ", "ltminigames.minigame.survive_the_tide_pvp_enabled.title": "¡ᗡƎꞀᗺⱯNƎ SI ԀΛԀ", + "ltminigames.minigame.team_won": "¡ǝɯɐb ǝɥʇ uoʍ %s ɯɐǝ⟘ ⭐", "ltminigames.minigame.teams.blue": "ǝnןᗺ", "ltminigames.minigame.teams.green": "uǝǝɹ⅁", "ltminigames.minigame.teams.hiders": "sɹǝpıH", diff --git a/src/generated/resources/assets/ltminigames/lang/en_us.json b/src/generated/resources/assets/ltminigames/lang/en_us.json index 1df521eb9..722c6b2e8 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_us.json +++ b/src/generated/resources/assets/ltminigames/lang/en_us.json @@ -15,6 +15,10 @@ "block.ltminigames.plastic_bottle": "Plastic Bottle", "block.ltminigames.plastic_rings": "Plastic Rings", "block.ltminigames.straw": "Straw", + "block.ltminigames.trivia_chest": "Trivia Chest", + "block.ltminigames.trivia_collectable": "Trivia Collectable", + "block.ltminigames.trivia_gate": "Trivia Gate", + "block.ltminigames.trivia_reward": "Trivia Reward", "effect.ltminigames.coin_multiplier_power_up": "Coin Multiplier Power-up", "effect.ltminigames.knockback_resistance_power_up": "Knockback Resistance Power-Up", "effect.ltminigames.leaky_pockets": "Leaky Pockets", @@ -66,6 +70,8 @@ "ltminigames.command.started_game": "You have started %s!", "ltminigames.command.stopped_game": "You have stopped %s!", "ltminigames.command.team_chat_intro": "You are using team chat. Use /shout or /chat global to chat with everyone.", + "ltminigames.container.triviaChest": "Trivia Chest", + "ltminigames.container.triviaChestDouble": "Large Trivia Chest", "ltminigames.minigame.arcade_turtle_race": "Arcade Turtle Race", "ltminigames.minigame.biodiversity_blitz": "Biodiversity Blitz", "ltminigames.minigame.biodiversity_blitz.can_only_place_plants": "You can only place plants you got from the shop!", @@ -164,7 +170,14 @@ "ltminigames.minigame.build_competition": "Build Competition", "ltminigames.minigame.calamity": "Calamity", "ltminigames.minigame.chaos_block_party": "Chaos Block Party", + "ltminigames.minigame.connect_four": "Connect Four", + "ltminigames.minigame.connect_four.teams_goes_next": "Team %s goes next", + "ltminigames.minigame.connect_four.your_turn": "It is your turn to place a block", "ltminigames.minigame.conservation_exploration": "Conservation Exploration", + "ltminigames.minigame.crafting_bee.dont_cheat": "Don't cheat!", + "ltminigames.minigame.crafting_bee.hint": "Click to show a hint, displaying the position of a random number of ingredients", + "ltminigames.minigame.crafting_bee.hints_left": "You have %s hints left", + "ltminigames.minigame.crafting_bee.team_has_completed_recipes": "Team %s has completed %s out of %s recipes", "ltminigames.minigame.donation.acid_rain": "Acid Rain", "ltminigames.minigame.donation.acid_rain.description": "Rain acid down on the players' heads for 1 minute!", "ltminigames.minigame.donation.acid_rain.toast": "It is raining acid for 1 minute!", @@ -382,6 +395,8 @@ "ltminigames.minigame.results": "The game is over! Here are the results:", "ltminigames.minigame.reward_item": " - %sx %s", "ltminigames.minigame.rewards_granted": "You got rewards for playing minigames!", + "ltminigames.minigame.river_race": "River Race", + "ltminigames.minigame.river_race.shop": "Shop", "ltminigames.minigame.signature_run": "Signature Run", "ltminigames.minigame.spectating_notification": "You are a %s!\nScroll or use the arrow keys to select players.\nHold %s and scroll to zoom.", "ltminigames.minigame.spectating_notification.key": "Left Control", @@ -465,6 +480,7 @@ "ltminigames.minigame.survive_the_tide_intro5": "\nLet's see!", "ltminigames.minigame.survive_the_tide_pvp_enabled.subtitle": "Beware of other players...", "ltminigames.minigame.survive_the_tide_pvp_enabled.title": "PVP IS ENABLED!", + "ltminigames.minigame.team_won": "⭐ Team %s won the game!", "ltminigames.minigame.teams.blue": "Blue", "ltminigames.minigame.teams.green": "Green", "ltminigames.minigame.teams.hiders": "Hiders", diff --git a/src/generated/resources/assets/ltminigames/models/block/trivia_chest.json b/src/generated/resources/assets/ltminigames/models/block/trivia_chest.json new file mode 100644 index 000000000..6c93c240a --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/block/trivia_chest.json @@ -0,0 +1,5 @@ +{ + "textures": { + "particle": "ltminigames:block/trivia_reward" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/block/trivia_collectable.json b/src/generated/resources/assets/ltminigames/models/block/trivia_collectable.json new file mode 100644 index 000000000..47b381d13 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/block/trivia_collectable.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "ltminigames:block/trivia_collectable" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/block/trivia_gate.json b/src/generated/resources/assets/ltminigames/models/block/trivia_gate.json new file mode 100644 index 000000000..b383b5712 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/block/trivia_gate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "ltminigames:block/trivia_gate" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/block/trivia_reward.json b/src/generated/resources/assets/ltminigames/models/block/trivia_reward.json new file mode 100644 index 000000000..bae5948b1 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/block/trivia_reward.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "ltminigames:block/trivia_reward" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/item/trivia_chest.json b/src/generated/resources/assets/ltminigames/models/item/trivia_chest.json new file mode 100644 index 000000000..678eba7b2 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/item/trivia_chest.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/chest", + "textures": { + "particle": "ltminigames:block/trivia_reward" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/item/trivia_collectable.json b/src/generated/resources/assets/ltminigames/models/item/trivia_collectable.json new file mode 100644 index 000000000..ffaa8c131 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/item/trivia_collectable.json @@ -0,0 +1,3 @@ +{ + "parent": "ltminigames:block/trivia_collectable" +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/item/trivia_gate.json b/src/generated/resources/assets/ltminigames/models/item/trivia_gate.json new file mode 100644 index 000000000..2b2188c1d --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/item/trivia_gate.json @@ -0,0 +1,3 @@ +{ + "parent": "ltminigames:block/trivia_gate" +} \ No newline at end of file diff --git a/src/generated/resources/assets/ltminigames/models/item/trivia_reward.json b/src/generated/resources/assets/ltminigames/models/item/trivia_reward.json new file mode 100644 index 000000000..79816d666 --- /dev/null +++ b/src/generated/resources/assets/ltminigames/models/item/trivia_reward.json @@ -0,0 +1,3 @@ +{ + "parent": "ltminigames:block/trivia_reward" +} \ No newline at end of file diff --git a/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json b/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json index 2fc5a8546..6fe72c750 100644 --- a/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json +++ b/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json @@ -19,6 +19,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json b/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json index 7fff7dc28..fefbf3b62 100644 --- a/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json +++ b/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json @@ -20,6 +20,7 @@ "volume": 0.5 } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json b/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json index 29213ea54..e36396c7a 100644 --- a/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json +++ b/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json @@ -16,6 +16,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json index 79341d30f..c347f9d9b 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json @@ -5,6 +5,7 @@ "type": "ltminigames:cancel_player_damage" } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json index ffc51ba0b..389204c6c 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json @@ -5,6 +5,7 @@ "type": "ltminigames:disable_hunger" } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json index ad4bb4ec5..27c5679f1 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json @@ -12,6 +12,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json index ced2dbb80..06390d600 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json @@ -12,6 +12,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/main/java/com/lovetropics/minigames/LoveTropics.java b/src/main/java/com/lovetropics/minigames/LoveTropics.java index 171df41b5..fe4341632 100644 --- a/src/main/java/com/lovetropics/minigames/LoveTropics.java +++ b/src/main/java/com/lovetropics/minigames/LoveTropics.java @@ -5,6 +5,7 @@ import com.lovetropics.minigames.client.game.handler.spectate.SpectatingUi; import com.lovetropics.minigames.client.lobby.LobbyKeybinds; import com.lovetropics.minigames.client.lobby.LobbyStateGui; +import com.lovetropics.minigames.client.render.block.TriviaChestRenderer; import com.lovetropics.minigames.common.config.ConfigLT; import com.lovetropics.minigames.common.content.MinigameTexts; import com.lovetropics.minigames.common.content.biodiversity_blitz.BiodiversityBlitz; @@ -15,9 +16,15 @@ import com.lovetropics.minigames.common.content.block_party.BlockParty; import com.lovetropics.minigames.common.content.block_party.BlockPartyTexts; import com.lovetropics.minigames.common.content.build_competition.BuildCompetition; +import com.lovetropics.minigames.common.content.connect4.ConnectFour; +import com.lovetropics.minigames.common.content.connect4.ConnectFourTexts; +import com.lovetropics.minigames.common.content.crafting_bee.CraftingBee; +import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; import com.lovetropics.minigames.common.content.hide_and_seek.HideAndSeek; import com.lovetropics.minigames.common.content.qottott.Qottott; import com.lovetropics.minigames.common.content.qottott.QottottTexts; +import com.lovetropics.minigames.common.content.river_race.RiverRace; +import com.lovetropics.minigames.common.content.river_race.RiverRaceTexts; import com.lovetropics.minigames.common.content.spleef.Spleef; import com.lovetropics.minigames.common.content.survive_the_tide.SurviveTheTide; import com.lovetropics.minigames.common.content.survive_the_tide.SurviveTheTideTexts; @@ -50,6 +57,7 @@ import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; import com.lovetropics.minigames.common.core.game.impl.GameEventDispatcher; import com.lovetropics.minigames.common.core.game.predicate.entity.EntityPredicates; +import com.lovetropics.minigames.common.core.game.predicate.loot.LootItemConditions; import com.lovetropics.minigames.common.core.game.util.GameTexts; import com.lovetropics.minigames.common.core.integration.BackendIntegrations; import com.lovetropics.minigames.common.core.item.MinigameDataComponents; @@ -61,6 +69,7 @@ import com.mojang.logging.LogUtils; import com.tterrag.registrate.providers.ProviderType; import net.minecraft.client.renderer.DimensionSpecialEffects; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; @@ -115,10 +124,13 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { MinigameTexts.KEYS.forEach(consumer); BiodiversityBlitzTexts.collectTranslations(consumer); BlockPartyTexts.KEYS.forEach(consumer); + CraftingBeeTexts.KEYS.forEach(consumer); + ConnectFourTexts.KEYS.forEach(consumer); SurviveTheTideTexts.KEYS.forEach(consumer); TrashDiveTexts.KEYS.forEach(consumer); TurtleRaceTexts.KEYS.forEach(consumer); QottottTexts.KEYS.forEach(consumer); + RiverRaceTexts.collectTranslations(consumer); }) .generic(TAB_ID.getPath(), Registries.CREATIVE_MODE_TAB, () -> CreativeModeTab.builder() .title(registrate().addLang("itemGroup", TAB_ID, "LTMinigames")) @@ -134,6 +146,7 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { GameBehaviorTypes.init(modBus); ActionTargetTypes.init(modBus); EntityPredicates.init(modBus); + LootItemConditions.init(); GameClientStateTypes.init(modBus); StreamHosts.init(); @@ -142,9 +155,12 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { SurviveTheTide.init(); TrashDive.init(); BlockParty.init(); + CraftingBee.init(); + ConnectFour.init(); TurtleRace.init(); Qottott.init(); Spleef.init(); + RiverRace.init(); DriftwoodRider.ATTACHMENT_TYPES.register(modBus); PlayerDisguise.ATTACHMENT_TYPES.register(modBus); @@ -220,6 +236,7 @@ public static class ClientSetup { @SubscribeEvent public static void setupClient(final FMLClientSetupEvent event) { LobbyKeybinds.init(); + BlockEntityRenderers.register(RiverRace.TRIVIA_CHEST_BLOCK_ENTITY.get(), TriviaChestRenderer::new); } @SubscribeEvent diff --git a/src/main/java/com/lovetropics/minigames/client/CustomItemRenderers.java b/src/main/java/com/lovetropics/minigames/client/CustomItemRenderers.java index d341426ac..ce2c54b59 100644 --- a/src/main/java/com/lovetropics/minigames/client/CustomItemRenderers.java +++ b/src/main/java/com/lovetropics/minigames/client/CustomItemRenderers.java @@ -1,11 +1,19 @@ package com.lovetropics.minigames.client; import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.river_race.RiverRace; +import com.lovetropics.minigames.common.content.river_race.block.TriviaChestBlock; +import com.lovetropics.minigames.common.content.river_race.block.TriviaChestBlockEntity; import com.lovetropics.minigames.common.core.diguise.DisguiseType; import com.lovetropics.minigames.common.core.item.MinigameDataComponents; +import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions; public class CustomItemRenderers { @@ -30,6 +38,16 @@ public static IClientItemExtensions plushieItem() { return createExtensions(new MobItemRenderer(minecraft.getBlockEntityRenderDispatcher(), minecraft.getEntityModels(), minecraft, stack -> stack.get(MinigameDataComponents.ENTITY), stack1 -> stack1.getOrDefault(MinigameDataComponents.SIZE, 1.0f), null)); } + public static IClientItemExtensions triviaChestItem(){ + TriviaChestBlockEntity triviaChestBlockEntity = new TriviaChestBlockEntity(BlockPos.ZERO, RiverRace.TRIVIA_CHEST.getDefaultState()); + return createExtensions(new BlockEntityWithoutLevelRenderer(Minecraft.getInstance().getBlockEntityRenderDispatcher(), Minecraft.getInstance().getEntityModels()) { + @Override + public void renderByItem(ItemStack stack, ItemDisplayContext displayContext, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) { + Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(triviaChestBlockEntity).render(triviaChestBlockEntity, 0, poseStack, buffer, packedLight, packedOverlay); + } + }); + } + private static IClientItemExtensions createExtensions(final BlockEntityWithoutLevelRenderer renderer) { return new IClientItemExtensions() { @Override diff --git a/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java new file mode 100644 index 000000000..17ae50e2d --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java @@ -0,0 +1,319 @@ +package com.lovetropics.minigames.client.game.handler; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; +import com.lovetropics.minigames.common.content.crafting_bee.SelectedRecipe; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.ChatFormatting; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterClientTooltipComponentFactoriesEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.function.Predicate; + +@EventBusSubscriber(modid = LoveTropics.ID, value = Dist.CLIENT) +public class GameCraftingBeeHandler { + private static int hintsRemaining; + private static UUID lastKnownGame; + private static Map hintGrids; + + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("ltminigames", "textures/gui/minigames/crafting_bee/items_bar.png"); + private static final ResourceLocation GRID_TEXTURE = ResourceLocation.fromNamespaceAndPath("ltminigames", "textures/gui/minigames/crafting_bee/crafting_grid.png"); + + @EventBusSubscriber(modid = LoveTropics.ID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) + public static class ModSubscriber { + @SubscribeEvent + static void onRegisterTooltips(final RegisterClientTooltipComponentFactoriesEvent event) { + event.register(RecipeHint.class, recipeHint -> new ClientTooltipComponent() { + @Override + public int getHeight() { + return 58; + } + + @Override + public int getWidth(Font font) { + return 54; + } + + @Override + public void renderImage(Font font, int x, int y, GuiGraphics guiGraphics) { + guiGraphics.blit(GRID_TEXTURE, x, y, 0, 0, 54, 54, 54, 54); + for (int i = 0; i < recipeHint.grid().size(); i++) { + var ingredient = recipeHint.grid.get(i); + if (ingredient.isEmpty()) continue; + + var size = recipeHint.grid.size() == 4 ? 2 : 3; + + guiGraphics.renderFakeItem( + resolveIngredient(ingredient), + x + 1 + 18 * (i % size), + y + 1 + 18 * (i / size) + ); + } + } + }); + } + } + + @SubscribeEvent + static void onGuiInit(ScreenEvent.Init.Post event) { + if (getState() == null || !(event.getScreen() instanceof CraftingScreen screen)) return; + + var state = getState(); + if (!Objects.equals(state.gameId(), lastKnownGame)) { + lastKnownGame = state.gameId(); + hintsRemaining = state.allowedHints(); + hintGrids = new HashMap<>(); + } + + event.addListener(new AbstractWidget(screen.getGuiLeft() + 22, screen.getGuiTop() - 21, 132, 21, Component.empty()) { + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + guiGraphics.blit(TEXTURE, this.getX(), this.getY(), 0, 0, 132, 21, 132, 21); + var crafts = getState().crafts(); + for (int i = 0; i < crafts.size(); i++) { + var craft = crafts.get(i); + var x = this.getX() + 4 + i * 18; + renderItem(guiGraphics, craft.output(), x, this.getY() + 4, 0, 0, 1f, 1f, 1f, craft.done() ? .1f : 1f); + + if (mouseX >= x && mouseX <= x + 16 && mouseY >= getY() + 4 && mouseY <= getY() + 20) { + var hint = hintGrids.get(craft.recipeId()); + + var tooltipLines = new ArrayList<>(Screen.getTooltipFromItem(Minecraft.getInstance(), craft.output())); + if (craft.done()) { + tooltipLines.set(0, tooltipLines.get(0).copy().withStyle(ChatFormatting.GREEN)); + } else if (hint == null || hint.expectedIngredientCount() != hint.grid().stream().filter(Predicate.not(Ingredient::isEmpty)).count()) { + tooltipLines.add(CraftingBeeTexts.HINT); + tooltipLines.add(CraftingBeeTexts.HINTS_LEFT.apply(Component.literal(String.valueOf(hintsRemaining)).withStyle(ChatFormatting.AQUA))); + } + guiGraphics.renderTooltip(Minecraft.getInstance().font, tooltipLines, Optional.ofNullable(hint).filter($ -> !craft.done()), mouseX, mouseY); + } + } + } + + @Override + public void onClick(double mouseX, double mouseY, int button) { + if (hintsRemaining <= 0) return; + + var crafts = getState().crafts(); + + if (mouseY < getY() + 4 || mouseY > getY() + 4 + 16) return; + if (mouseX < getX() + 4 || mouseX > getX() + 4 + (18 * crafts.size() - 1)) return; + var index = (int)(mouseX - getX() - 4) / 18; + + var craft = crafts.get(index); + if (craft.done()) return; + + var recipe = new SelectedRecipe(craft.recipeId(), Minecraft.getInstance().player.connection.getRecipeManager()); + var ingredients = recipe.decompose(); + + var grid = hintGrids.computeIfAbsent(craft.recipeId(), k -> new RecipeHint( + NonNullList.withSize( + recipe.recipe().map(shaped -> shaped.getWidth() * shaped.getHeight(), shapeless -> shapeless.getIngredients().size() > 3 ? 9 : shapeless.getIngredients().size()), + Ingredient.EMPTY), + (int)ingredients.stream().filter(Predicate.not(Ingredient::isEmpty)).count() + )); + + int filledGridAmount = (int) grid.grid().stream().filter(Predicate.not(Ingredient::isEmpty)).count(); + if (grid.expectedIngredientCount() == filledGridAmount) return; + + record PositionedIngredient(Ingredient ingredient, int position) {} + + List ingredientsToPick = new ArrayList<>(); + for (int i = 0; i < ingredients.size(); i++) { + var ingr = ingredients.get(i); + if (!ingr.isEmpty() && grid.grid().get(i).isEmpty()) { + ingredientsToPick.add(new PositionedIngredient(ingr, i)); + } + } + + Collections.shuffle(ingredientsToPick); + // Make sure that we never show the full recipe in just one hint + var ingredientsToShow = new Random().nextInt(filledGridAmount == 0 ? Math.max(1, ingredientsToPick.size() - 1) : ingredientsToPick.size()); + + for (int i = 0; i <= ingredientsToShow; i++) { + var ingredient = ingredientsToPick.get(i); + grid.grid().set(ingredient.position(), ingredient.ingredient()); + } + + hintsRemaining--; + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + + } + }); + } + + @Nullable + private static CraftingBeeCraftsClientState getState() { + return ClientGameStateManager.getOrNull(GameClientStateTypes.CRAFTING_BEE_CRAFTS); + } + + private static ItemStack resolveIngredient(Ingredient ingredient) { + if (ingredient.isEmpty()) { + return ItemStack.EMPTY; + } + for (ItemStack item : ingredient.getItems()) { + // Prioritize vanilla items + if (item.getItem().builtInRegistryHolder().key().location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { + return item; + } + } + return ingredient.getItems()[0]; + } + + public static void reset() { + + } + + private static void renderItem( + GuiGraphics graphics, ItemStack stack, int x, int y, int seed, int guiOffset, + float redTint, float greenTint, float blueTint, float alphaTint + ) { + if (!stack.isEmpty()) { + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + + BakedModel bakedmodel = Minecraft.getInstance().getItemRenderer().getModel(stack, null, null, seed); + graphics.pose().pushPose(); + graphics.pose().translate((float) (x + 8), (float) (y + 8), (float) (150 + (bakedmodel.isGui3d() ? guiOffset : 0))); + + try { + graphics.pose().scale(16.0F, -16.0F, 16.0F); + RenderSystem.applyModelViewMatrix(); + + boolean flag = !bakedmodel.usesBlockLight(); + if (flag) { + Lighting.setupForFlatItems(); + } + + Minecraft.getInstance() + .getItemRenderer() + .render(stack, ItemDisplayContext.GUI, false, graphics.pose(), renderType -> { + if (renderType instanceof RenderType.CompositeRenderType composite) { + if (composite.state().textureState instanceof RenderStateShard.TextureStateShard texture && texture.texture.isPresent()) { + return new TintedVertexConsumer( + graphics.bufferSource().getBuffer(RenderType.entityTranslucent(texture.texture.get())), redTint, greenTint, blueTint, alphaTint + ); + } + } + return new TintedVertexConsumer( + graphics.bufferSource().getBuffer(renderType), redTint, greenTint, blueTint, alphaTint + ); + }, LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, bakedmodel); + graphics.flush(); + RenderSystem.enableDepthTest(); + if (flag) { + Lighting.setupFor3DItems(); + } + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Rendering item"); + CrashReportCategory crashreportcategory = crashreport.addCategory("Item being rendered"); + crashreportcategory.setDetail("Item Type", () -> String.valueOf(stack.getItem())); + crashreportcategory.setDetail("Item Components", () -> String.valueOf(stack.getComponents())); + crashreportcategory.setDetail("Item Foil", () -> String.valueOf(stack.hasFoil())); + throw new ReportedException(crashreport); + } + + graphics.pose().popPose(); + RenderSystem.applyModelViewMatrix(); + } + } + + public static final class TintedVertexConsumer implements VertexConsumer { + private final VertexConsumer wrapped; + + @Override + public VertexConsumer addVertex(float x, float y, float z) { + return wrapped.addVertex(x, y, z); + } + + @Override + public VertexConsumer setColor(int red, int green, int blue, int alpha) { + return wrapped.setColor((int)(red * this.red), (int)(green * this.green), (int)(blue * this.blue), (int)(alpha * this.alpha)); + } + + @Override + public VertexConsumer setUv(float u, float v) { + return wrapped.setUv(u, v); + } + + @Override + public VertexConsumer setUv1(int u, int v) { + return wrapped.setUv1(u, v); + } + + @Override + public VertexConsumer setUv2(int u, int v) { + return wrapped.setUv2(u, v); + } + + @Override + public VertexConsumer setNormal(float normalX, float normalY, float normalZ) { + return wrapped.setNormal(normalX, normalY, normalZ); + } + + private final float red; + private final float green; + private final float blue; + private final float alpha; + + public TintedVertexConsumer(VertexConsumer wrapped, float red, float green, float blue, float alpha) { + this.wrapped = wrapped; + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + } + + public record RecipeHint( + NonNullList grid, + int expectedIngredientCount + ) implements TooltipComponent {} + +} diff --git a/src/main/java/com/lovetropics/minigames/client/game/trivia/ClientTriviaHandler.java b/src/main/java/com/lovetropics/minigames/client/game/trivia/ClientTriviaHandler.java new file mode 100644 index 000000000..b35cf922d --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/client/game/trivia/ClientTriviaHandler.java @@ -0,0 +1,29 @@ +package com.lovetropics.minigames.client.game.trivia; + +import com.lovetropics.minigames.common.core.network.trivia.ShowTriviaMessage; +import com.lovetropics.minigames.common.core.network.trivia.TriviaAnswerResponseMessage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; + +public class ClientTriviaHandler { + + public static void showScreen(ShowTriviaMessage message){ + final ClientLevel level = Minecraft.getInstance().level; + if (level == null) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + minecraft.setScreen(new TriviaQuestionScreen(message.triviaBlock(), message.question(), message.triviaBlockState())); + } + + public static void handleResponse(TriviaAnswerResponseMessage message) { + final ClientLevel level = Minecraft.getInstance().level; + if (level == null) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.screen instanceof TriviaQuestionScreen triviaQuestionScreen) { + triviaQuestionScreen.handleAnswerResponse(message.triviaBlockState()); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/client/game/trivia/TriviaQuestionScreen.java b/src/main/java/com/lovetropics/minigames/client/game/trivia/TriviaQuestionScreen.java new file mode 100644 index 000000000..a802ecabf --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/client/game/trivia/TriviaQuestionScreen.java @@ -0,0 +1,181 @@ +package com.lovetropics.minigames.client.game.trivia; + +import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import com.lovetropics.minigames.common.core.network.trivia.SelectTriviaAnswerMessage; +import com.lovetropics.minigames.common.core.network.trivia.RequestTriviaStateUpdateMessage; +import com.lovetropics.minigames.common.core.network.trivia.SelectTriviaAnswerMessage; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.*; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.GridLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import net.neoforged.neoforge.network.PacketDistributor; + +public class TriviaQuestionScreen extends Screen { + + public static class AutoUpdatingTextWidget extends AbstractStringWidget { + private float alignX; + private Supplier messageSupplier; + + public AutoUpdatingTextWidget(Supplier message, Font font) { + super(0, 0, 0, 0, Component.empty(), font); + this.messageSupplier = message; + } + + public AutoUpdatingTextWidget setColor(int pColor) { + super.setColor(pColor); + return this; + } + + private AutoUpdatingTextWidget horizontalAlignment(float pHorizontalAlignment) { + this.alignX = pHorizontalAlignment; + return this; + } + + public AutoUpdatingTextWidget alignLeft() { + return this.horizontalAlignment(0.0F); + } + + public AutoUpdatingTextWidget alignCenter() { + return this.horizontalAlignment(0.5F); + } + + public AutoUpdatingTextWidget alignRight() { + return this.horizontalAlignment(1.0F); + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int i, int i1, float v) { + Component component = messageSupplier.get(); + Font font = this.getFont(); + MultiLineLabel label = MultiLineLabel.create(font, component); + int x = this.getX(); + int y = this.getY(); + label.renderCentered(guiGraphics, x, y, 9, getColor()); +// guiGraphics.drawString(font, component, x, totalY, this.getColor()); + } + } + + //TODO: make this a thing that actually looks like a good design + public static class AnswerButton extends Button { + protected AnswerButton(Builder builder) { + super(builder); + } + + public void setActive(boolean active){ + this.active = active; + if(active){ + setAlpha(1); + } else { + setAlpha(0.5f); + } + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.renderWidget(guiGraphics, mouseX, mouseY, partialTick); + } + } + + private TriviaBehaviour.TriviaQuestion question; + private final BlockPos triviaBlockPos; + private TriviaBlockEntity.TriviaBlockState triviaBlockState; + private final GridLayout layout = new GridLayout().spacing(25); + private String selected = null; + private Set buttons = new HashSet<>(); + + public TriviaQuestionScreen(BlockPos triviaBlockPos, TriviaBehaviour.TriviaQuestion question, TriviaBlockEntity.TriviaBlockState blockState) { + super(Component.literal("Answer Trivia Question")); + this.triviaBlockPos = triviaBlockPos; + this.question = question; + this.triviaBlockState = blockState; + } + + @Override + protected void init() { + buttons.clear(); + GridLayout.RowHelper helper = layout.createRowHelper(1); + layout.defaultCellSetting().alignHorizontallyCenter(); + helper.addChild(new AutoUpdatingTextWidget(() -> { + if(triviaBlockState.lockedOut()) { + return Component.literal("LOCKED OUT!\n") + .append(Component.literal("Unlocks in " + (triviaBlockState.unlocksAt() - System.currentTimeMillis()) / 1000 + "s")) + .withStyle(ChatFormatting.RED); + } else if(triviaBlockState.correctAnswer().isPresent()){ + return Component.literal("Answered Correctly!").withStyle(ChatFormatting.GREEN); + } + return Component.empty(); + }, font).alignCenter(), 1); + helper.addChild(new MultiLineTextWidget(Component.literal(question.question()), font).setCentered(true), 1); + for (TriviaBehaviour.TriviaQuestion.TriviaQuestionAnswer answer : question.answers()) { + AnswerButton button = new AnswerButton(Button.builder(Component.literal(answer.text()), this::handleAnswerClick)); + button.setActive(getButtonState(answer.text())); + buttons.add(button); + helper.addChild(button, 1); + } + repositionElements(); + layout.visitWidgets(this::addRenderableWidget); + } + + private boolean getButtonState(String answerText){ + boolean isEnabled = true; + if(triviaBlockState.lockedOut()){ + isEnabled = false; + } else { + if (triviaBlockState.isAnswered() && triviaBlockState.correctAnswer().isPresent()) { + if (!triviaBlockState.correctAnswer().get().equals(answerText)) { + isEnabled = false; + } + } + } + return isEnabled; + } + + @Override + public void tick() { + super.tick(); + if(triviaBlockState.lockedOut() && triviaBlockState.unlocksAt() <= System.currentTimeMillis()){ + PacketDistributor.sendToServer(new RequestTriviaStateUpdateMessage(triviaBlockPos)); + } + } + + private void handleAnswerClick(Button clickedButton){ + if(triviaBlockState.isAnswered()){ + return; + } + String selectedAnswer = clickedButton.getMessage().getString(); + selected = selectedAnswer; + PacketDistributor.sendToServer(new SelectTriviaAnswerMessage(triviaBlockPos, selectedAnswer)); + } + + public void handleAnswerResponse(TriviaBlockEntity.TriviaBlockState triviaBlockState){ + this.triviaBlockState = triviaBlockState; + for (AnswerButton button : buttons) { + button.setActive(getButtonState(button.getMessage().getString())); + } + } + + @Override + protected void repositionElements() { + layout.arrangeElements(); + FrameLayout.centerInRectangle(layout, 0, 0, width, height); + } + + @Override + public boolean isPauseScreen() { + return false; + } +} diff --git a/src/main/java/com/lovetropics/minigames/client/game/trivia/package-info.java b/src/main/java/com/lovetropics/minigames/client/game/trivia/package-info.java new file mode 100644 index 000000000..3f5e59d97 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/client/game/trivia/package-info.java @@ -0,0 +1,9 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +package com.lovetropics.minigames.client.game.trivia; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/com/lovetropics/minigames/client/render/block/TriviaChestRenderer.java b/src/main/java/com/lovetropics/minigames/client/render/block/TriviaChestRenderer.java new file mode 100644 index 000000000..3f1c3bd88 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/client/render/block/TriviaChestRenderer.java @@ -0,0 +1,36 @@ +package com.lovetropics.minigames.client.render.block; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.river_race.block.TriviaChestBlockEntity; +import net.minecraft.client.renderer.Sheets; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.ChestRenderer; +import net.minecraft.client.resources.model.Material; +import net.minecraft.world.level.block.state.properties.ChestType; + +public class TriviaChestRenderer extends ChestRenderer { + + public static final Material TRIVIA_CHEST_MATERIAL = getChestMaterial("trivia"); + public static final Material TRIVIA_CHEST_LEFT_MATERIAL = getChestMaterial("trivia_left"); + public static final Material TRIVIA_CHEST_RIGHT_MATERIAL = getChestMaterial("trivia_right"); + + private static Material getChestMaterial(ChestType chestType) { + return switch (chestType) { + case LEFT -> TriviaChestRenderer.TRIVIA_CHEST_LEFT_MATERIAL; + case RIGHT -> TriviaChestRenderer.TRIVIA_CHEST_RIGHT_MATERIAL; + default -> TriviaChestRenderer.TRIVIA_CHEST_MATERIAL; + }; + } + + private static Material getChestMaterial(String chestName) { + return new Material(Sheets.CHEST_SHEET, LoveTropics.location("entity/chest/" + chestName)); + } + public TriviaChestRenderer(BlockEntityRendererProvider.Context context) { + super(context); + } + + protected Material getMaterial(TriviaChestBlockEntity tileEntity, ChestType chestType) { + return getChestMaterial(chestType); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java b/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java index f93bea283..70edb0b79 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java +++ b/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java @@ -34,6 +34,7 @@ public final class MinigameTexts { public static final Component CHAOS_BLOCK_PARTY = KEYS.add("chaos_block_party", "Chaos Block Party"); public static final Component LEVITATION = KEYS.add("levitation", "Levitation"); public static final Component QOTTOTT = KEYS.add("qottott", "Qottott"); + public static final Component CONNECT_FOUR = KEYS.add("connect_four", "Connect Four"); // TODO: These should move into SurviveTheTideTexts public static final Component[] SURVIVE_THE_TIDE_INTRO = { @@ -67,6 +68,7 @@ public final class MinigameTexts { public static final Component WINNER_TITLE = KEYS.add("winner.title", "WINNER"); public static final Component WINNER_SUBTITLE = KEYS.add("winner.subtitle", "You've emerged victorious!"); + public static final TranslationCollector.Fun1 TEAM_WON = KEYS.add1("team_won", "⭐ Team %s won the game!"); public static final TranslationCollector.Fun1 PLAYER_WON = KEYS.add1("player_won", "⭐ %s won the game!"); public static final Component NOBODY_WON = KEYS.add("nobody_won", "⭐ Nobody won the game!"); public static final Component RESULTS = KEYS.add("results", "The game is over! Here are the results:"); diff --git a/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java index 9e6fe9b13..7e8e7eaa5 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java @@ -9,6 +9,7 @@ import com.lovetropics.minigames.common.core.game.SpawnBuilder; import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; import com.lovetropics.minigames.common.core.game.player.PlayerRole; @@ -171,6 +172,8 @@ private void tick() { if (winningPlayer != null) { message = MinigameTexts.PLAYER_WON.apply(winningPlayer.getDisplayName()).withStyle(ChatFormatting.GREEN); game.statistics().global().set(StatisticKey.WINNING_PLAYER, PlayerKey.from(winningPlayer)); + + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(winningPlayer.getDisplayName()); } else { message = MinigameTexts.NOBODY_WON.copy().withStyle(ChatFormatting.RED); } diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java new file mode 100644 index 000000000..60b7f15dd --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java @@ -0,0 +1,16 @@ +package com.lovetropics.minigames.common.content.connect4; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.util.registry.GameBehaviorEntry; +import com.lovetropics.minigames.common.util.registry.LoveTropicsRegistrate; + +public class ConnectFour { + private static final LoveTropicsRegistrate REGISTRATE = LoveTropics.registrate(); + + public static final GameBehaviorEntry CONNECT_FOUR = REGISTRATE.object("connect_four") + .behavior(ConnectFourBehavior.CODEC) + .register(); + + public static void init() { + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java new file mode 100644 index 000000000..b8255e2a4 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java @@ -0,0 +1,254 @@ +package com.lovetropics.minigames.common.content.connect4; + +import com.lovetropics.lib.BlockBox; +import com.lovetropics.lib.codec.MoreCodecs; +import com.lovetropics.minigames.common.content.MinigameTexts; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.GameStopReason; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GameWorldEvents; +import com.lovetropics.minigames.common.core.game.state.statistics.PlayerKey; +import com.lovetropics.minigames.common.core.game.state.statistics.StatisticKey; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import com.lovetropics.minigames.common.util.SequentialList; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +public class ConnectFourBehavior implements IGameBehavior { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Codec.unboundedMap(GameTeamKey.CODEC, GameBlock.CODEC.codec()).fieldOf("team_blocks").forGetter(c -> c.teamBlocks), + Codec.STRING.fieldOf("placing_region").forGetter(c -> c.placingRegionKey), + MoreCodecs.BLOCK_STATE.fieldOf("separator").forGetter(c -> c.separator), + MoreCodecs.BLOCK_STATE.fieldOf("blocker").forGetter(c -> c.blocker), + Codec.INT.fieldOf("grid_width").forGetter(c -> c.width), + Codec.INT.fieldOf("grid_height").forGetter(c -> c.height), + Codec.INT.optionalFieldOf("connect", 4).forGetter(c -> c.connectAmount) + ).apply(in, ConnectFourBehavior::new)); + + private final Map teamBlocks; + private final String placingRegionKey; + private final BlockState separator; + private final BlockState blocker; + + private final int width, height, connectAmount; + + public ConnectFourBehavior(Map teamBlocks, String placingRegionKey, BlockState separator, BlockState blocker, int width, int height, int connectAmount) { + this.teamBlocks = teamBlocks; + this.placingRegionKey = placingRegionKey; + this.separator = separator; + this.blocker = blocker; + this.width = width; + this.height = height; + this.connectAmount = connectAmount; + } + + private IGamePhase game; + + @Nullable + private PendingGate pendingGate; + + private SequentialList playingTeams; + + private TeamState teams; + private BlockBox placingRegion; + + private GameTeamKey[][] pieces; + private int placedPieces; + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + this.game = game; + placingRegion = game.mapRegions().getOrThrow(placingRegionKey); + teams = game.instanceState().getOrThrow(TeamState.KEY); + + pieces = new GameTeamKey[width][height]; + placedPieces = 0; + + events.listen(GamePhaseEvents.START, this::onStart); + + events.listen(GamePlayerEvents.PLACE_BLOCK, this::onPlaceBlock); + events.listen(GamePlayerEvents.BREAK_BLOCK, (player, pos, state, hand) -> player.isCreative() ? InteractionResult.PASS : InteractionResult.FAIL); + + events.listen(GameWorldEvents.BLOCK_LANDED, this::onBlockLanded); + } + + private void onStart() { + var teams = new ArrayList(this.teams.getTeamKeys().size()); + this.teams.getTeamKeys().forEach(key -> { + var players = this.teams.getPlayersForTeam(key).stream().map(PlayerKey::from).toList(); + if (!players.isEmpty()) { + teams.add(new PlayingTeam(key, new SequentialList<>(players, -1))); + } + }); + Collections.shuffle(teams); + playingTeams = new SequentialList<>(teams, -1); + + nextPlayer(); + } + + private InteractionResult onPlaceBlock(ServerPlayer player, BlockPos pos, BlockState placed, BlockState placedOn) { + if (player.isCreative()) return InteractionResult.PASS; + + if (!Objects.equals(playingTeams.current().players.current(), PlayerKey.from(player)) || !placingRegion.contains(pos)) + return InteractionResult.FAIL; + + var expected = teamBlocks.get(playingTeams.current().key).powder; + if (expected != placed.getBlock()) return InteractionResult.FAIL; + + var below = pos.below(); + pendingGate = new PendingGate(below, player.level().getBlockState(below)); + player.level().setBlock(below, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + + return InteractionResult.PASS; + } + + private void onBlockLanded(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + var expected = teamBlocks.get(playingTeams.current().key); + + if (!state.is(expected.powder)) return; + + level.setBlock(pos, expected.solid.defaultBlockState(), Block.UPDATE_ALL); + + if (pendingGate != null) { + level.setBlock(pendingGate.gatePosition(), pendingGate.gate(), Block.UPDATE_ALL); + pendingGate = null; + } + + level.setBlock(pos.above(), separator, Block.UPDATE_ALL); + // Separator above, and gate above that + if (placingRegion.contains(pos.getX(), pos.getY() + 3, pos.getZ())) { + level.setBlock(pos.above(3), blocker, Block.UPDATE_ALL); + } + + int x = (pos.getX() - placingRegion.min().getX()) / 2; + var column = pieces[x]; + int y; + for (y = 0; y < column.length; y++) { + if (column[y] == null) { + column[y] = playingTeams.current().key(); + break; + } + } + placedPieces++; + + var team = playingTeams.current().key(); + + game.allPlayers().getPlayerBy(playingTeams.current().players().current()).setGlowingTag(false); + + if (checkWin(x, y, team)) { + game.statistics().global().set(StatisticKey.WINNING_TEAM, team); + var teamConfig = teams.getTeamByKey(team).config(); + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(teamConfig.styledName()); + game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); + + game.allPlayers().forEach(ServerPlayer::closeContainer); + + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamConfig.styledName()).withStyle(ChatFormatting.GREEN), true)); + game.schedule(5, () -> game.requestStop(GameStopReason.finished())); + } else { + if (placedPieces == width * height) { + game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); + + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.NOBODY_WON, true)); + game.schedule(5, () -> game.requestStop(GameStopReason.finished())); + } else { + nextPlayer(); + } + } + } + + private void nextPlayer() { + var nextTeam = playingTeams.next(); + var nextPlayer = nextTeam.players().next(); + + game.allPlayers().sendMessage(ConnectFourTexts.TEAM_GOES_NEXT.apply(teams.getTeamByKey(nextTeam.key).config().styledName()), false); + + var player = game.allPlayers().getPlayerBy(nextPlayer); + player.addItem(teamBlocks.get(nextTeam.key).powder.asItem().getDefaultInstance()); + + player.setGlowingTag(true); + player.displayClientMessage(ConnectFourTexts.IT_IS_YOUR_TURN.copy().withStyle(ChatFormatting.GOLD), true); + } + + private boolean checkWin(int x, int y, GameTeamKey team) { + if (checkLine(x, y, 0, -1, team)) { // vertical + return true; + } + + for (int offset = 0; offset < connectAmount; offset++) { + if (checkLine(x - (connectAmount - 1) + offset, y, 1, 0, team)) { // horizontal + return true; + } + + if (checkLine(x - (connectAmount + 1) + offset, y + (connectAmount - 1) - offset, 1, -1, team)) { // leading diagonal + return true; + } + + if (checkLine(x - (connectAmount + 1) + offset, y - (connectAmount + 1) + offset, 1, 1, team)) { // trailing diagonal + return true; + } + } + + return false; + } + + + private boolean checkLine(int xs, int ys, int dx, int dy, GameTeamKey team) { + for (int i = 0; i < connectAmount; i++) { + int x = xs + (dx * i); + int y = ys + (dy * i); + + if (x < 0 || x > pieces.length - 1) { + return false; + } + + if (y < 0 || y > pieces[x].length - 1) { + return false; + } + + if (team != pieces[x][y]) { + return false; + } + } + + return true; + } + + private record GameBlock(Block powder, Block solid) { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + BuiltInRegistries.BLOCK.byNameCodec().fieldOf("powder").forGetter(GameBlock::powder), + BuiltInRegistries.BLOCK.byNameCodec().fieldOf("solid").forGetter(GameBlock::solid) + ).apply(in, GameBlock::new)); + } + + private record PendingGate(BlockPos gatePosition, BlockState gate) { + } + + private record PlayingTeam(GameTeamKey key, SequentialList players) { + + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java new file mode 100644 index 000000000..9588c35b1 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java @@ -0,0 +1,13 @@ +package com.lovetropics.minigames.common.content.connect4; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.core.game.util.TranslationCollector; +import net.minecraft.network.chat.Component; + +public class ConnectFourTexts { + public static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.connect_four."); + + public static final Component IT_IS_YOUR_TURN = KEYS.add("your_turn", "It is your turn to place a block"); + + public static final TranslationCollector.Fun1 TEAM_GOES_NEXT = KEYS.add1("teams_goes_next", "Team %s goes next"); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java new file mode 100644 index 000000000..84dc60820 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java @@ -0,0 +1,16 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.util.registry.GameBehaviorEntry; +import com.lovetropics.minigames.common.util.registry.LoveTropicsRegistrate; + +public class CraftingBee { + private static final LoveTropicsRegistrate REGISTRATE = LoveTropics.registrate(); + + public static final GameBehaviorEntry CRAFTING_BEE = REGISTRATE.object("crafting_bee") + .behavior(CraftingBeeBehavior.CODEC) + .register(); + + public static void init() { + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java new file mode 100644 index 000000000..1c955aacc --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java @@ -0,0 +1,213 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import com.lovetropics.minigames.common.content.MinigameTexts; +import com.lovetropics.minigames.common.content.crafting_bee.ingredient.IngredientDecomposer; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.GameStopReason; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.GameBehaviorType; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; +import com.lovetropics.minigames.common.core.game.player.PlayerSet; +import com.lovetropics.minigames.common.core.game.state.statistics.StatisticKey; +import com.lovetropics.minigames.common.core.game.state.team.GameTeam; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.BlockHitResult; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CraftingBeeBehavior implements IGameBehavior { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + RecipeSelector.CODEC.listOf().fieldOf("selectors").forGetter(c -> c.selectors), + IngredientDecomposer.CODEC.codec().listOf().fieldOf("decomposers").forGetter(c -> c.decomposers), + Codec.INT.optionalFieldOf("hints_per_player", 3).forGetter(c -> c.allowedHints) + ).apply(in, CraftingBeeBehavior::new)); + + private final List selectors; + private final List decomposers; + private final int allowedHints; + + private TeamState teams; + private IGamePhase game; + + private ListMultimap tasks; + private volatile boolean done; + + public CraftingBeeBehavior(List selectors, List decomposers, int allowedHints) { + this.selectors = selectors; + this.decomposers = decomposers; + this.allowedHints = allowedHints; + } + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + tasks = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); + + decomposers.forEach(dec -> dec.prepareCache(game.level())); + + this.game = game; + teams = game.instanceState().getOrThrow(TeamState.KEY); + + events.listen(GamePhaseEvents.START, this::start); + events.listen(GamePlayerEvents.CRAFT, this::onCraft); + events.listen(GamePlayerEvents.USE_BLOCK, this::useBlock); + + events.listen(GamePhaseEvents.STOP, reason -> GameClientState.removeFromPlayers(GameClientStateTypes.CRAFTING_BEE_CRAFTS.get(), game.allPlayers())); + events.listen(GamePlayerEvents.REMOVE, player -> GameClientState.removeFromPlayer(GameClientStateTypes.CRAFTING_BEE_CRAFTS.get(), player)); + } + + private void start() { + for (GameTeam team : teams) { + var recipes = selectors.stream().map(selector -> selector.select(game.level())) + .map(recipe -> new CraftingTask( + recipe.getResult(game.registryAccess()), + recipe + )) + .toList(); + tasks.putAll(team.key(), recipes); + sync(team.key()); + distributeIngredients(recipes, teams.getPlayersForTeam(team.key())); + } + } + + private void distributeIngredients(Collection tasks, PlayerSet players) { + // Empty teams have no players to distribute items to + if (players.isEmpty()) return; + + for (CraftingTask task : tasks) { + var ingredients = task.recipe.decompose(); + var items = ingredients.stream().flatMap(this::singleDecomposition).collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(items); + + // Evenly distribute the items between the players + int p = 0; + var playerList = players.stream().toList(); + for (ItemStack item : items) { + playerList.get(p++).addItem(item.copy()); + if (p >= playerList.size()) p = 0; + } + } + } + + private Stream singleDecomposition(Ingredient ingredient) { + if (ingredient.isEmpty()) return Stream.empty(); + + for (IngredientDecomposer decomposer : decomposers) { + var decomposed = decomposer.decompose(ingredient); + if (decomposed != null) { + return decomposed.stream().flatMap(this::singleDecomposition); + } + } + + // We have reduced the ingredient to its most basic form, so now we just pick the first item of the ingredient + for (ItemStack item : ingredient.getItems()) { + // Prioritize vanilla items + if (item.getItem().builtInRegistryHolder().key().location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { + return Stream.of(item); + } + } + return Stream.of(ingredient.getItems()[0]); + } + + private void onCraft(Player player, ItemStack crafted, Container container) { + var team = teams.getTeamForPlayer(player); + if (team == null || done) return; + + var teamTasks = tasks.get(team); + var task = teamTasks.stream().filter(c -> ItemStack.isSameItemSameComponents(crafted, c.output)).findFirst().orElse(null); + if (task == null || task.done) return; + + task.done = true; + + sync(team); + + var completed = teamTasks.stream().filter(t -> t.done).count(); + var teamConfig = teams.getTeamByKey(team).config(); + + game.allPlayers().sendMessage(CraftingBeeTexts.TEAM_HAS_COMPLETED_RECIPES.apply(teamConfig.styledName(), completed, teamTasks.size())); + + if (completed == teamTasks.size()) { + + game.statistics().global().set(StatisticKey.WINNING_TEAM, team); + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(teamConfig.name()); + game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); + + done = true; + + game.allPlayers().forEach(ServerPlayer::closeContainer); + + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamConfig.styledName()).withStyle(ChatFormatting.GREEN), true)); + game.schedule(5, () -> game.requestStop(GameStopReason.finished())); + } + } + + private InteractionResult useBlock(ServerPlayer player, ServerLevel world, BlockPos pos, InteractionHand hand, BlockHitResult traceResult) { + // don't allow players to use the crafting table after the game was won + if (world.getBlockState(pos).is(Blocks.CRAFTING_TABLE) && done) { + return InteractionResult.FAIL; + } + return InteractionResult.PASS; + } + + private void sync(GameTeamKey team) { + teams.getPlayersForTeam(team).forEach(this::sync); + } + + private void sync(Player player) { + if (player instanceof ServerPlayer sp) { + GameClientState.sendToPlayer(new CraftingBeeCraftsClientState(tasks.get(teams.getTeamForPlayer(player)).stream().map(CraftingTask::toCraft).toList(), + game.gameUuid(), allowedHints), sp); + } + } + + @Override + public Supplier> behaviorType() { + return CraftingBee.CRAFTING_BEE; + } + + private static class CraftingTask { + private final ItemStack output; + private final SelectedRecipe recipe; + private boolean done; + + private CraftingTask(ItemStack output, SelectedRecipe recipe) { + this.output = output; + this.recipe = recipe; + } + + public CraftingBeeCraftsClientState.Craft toCraft() { + return new CraftingBeeCraftsClientState.Craft(output, recipe.id(), done); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java new file mode 100644 index 000000000..9c4fd32cc --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java @@ -0,0 +1,15 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.core.game.util.TranslationCollector; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public final class CraftingBeeTexts { + public static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.crafting_bee."); + + public static final TranslationCollector.Fun3 TEAM_HAS_COMPLETED_RECIPES = KEYS.add3("team_has_completed_recipes", "Team %s has completed %s out of %s recipes"); + public static final Component DONT_CHEAT = KEYS.add("dont_cheat", "Don't cheat!").withStyle(ChatFormatting.RED); + public static final Component HINT = KEYS.add("hint", "Click to show a hint, displaying the position of a random number of ingredients"); + public static final TranslationCollector.Fun1 HINTS_LEFT = KEYS.add1("hints_left", "You have %s hints left"); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java new file mode 100644 index 000000000..a611af8a8 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java @@ -0,0 +1,93 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.Util; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; + +import java.util.List; +import java.util.Optional; + +public interface RecipeSelector { + BiMap> TYPES = ImmutableBiMap.of("from_list", FromList.CODEC, "one_of", OneOf.CODEC, "from_item_tag", FromItemTag.CODEC); + Codec CODEC = Codec.STRING.dispatch(s -> TYPES.inverse().get(s.getType()), TYPES::get); + + SelectedRecipe select(ServerLevel level); + + MapCodec getType(); + + record FromList(List recipes) implements RecipeSelector { + public static final MapCodec CODEC = ResourceLocation.CODEC.listOf().fieldOf("recipes") + .xmap(FromList::new, FromList::recipes); + + @Override + public SelectedRecipe select(ServerLevel level) { + Optional> recipe = Optional.empty(); + while (recipe.isEmpty()) { + var key = Util.getRandom(recipes, level.getRandom()); + recipe = level.getRecipeManager().byKey(key); + if (recipe.isEmpty()) { + LogUtils.getLogger().error("Recipe '{}' doesn't exist", key); + } + } + return new SelectedRecipe(recipe.get()); + } + + @Override + public MapCodec getType() { + return CODEC; + } + } + + record OneOf(List selectors) implements RecipeSelector { + public static final MapCodec CODEC = MapCodec.assumeMapUnsafe(Codec.lazyInitialized(() -> RecipeSelector.CODEC.listOf().fieldOf("selectors") + .xmap(OneOf::new, OneOf::selectors).codec())); + @Override + public SelectedRecipe select(ServerLevel level) { + return Util.getRandom(selectors, level.getRandom()).select(level); + } + + @Override + public MapCodec getType() { + return CODEC; + } + } + + class FromItemTag implements RecipeSelector { + public static final MapCodec CODEC = TagKey.hashedCodec(Registries.ITEM).fieldOf("tag") + .xmap(FromItemTag::new, s -> s.tag); + + private final TagKey tag; + + private List cache; + + public FromItemTag(TagKey tag) { + this.tag = tag; + } + + @Override + public SelectedRecipe select(ServerLevel level) { + if (cache == null) { + cache = level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING) + .stream().filter(h -> h.value().getResultItem(level.registryAccess()).is(tag) && h.id().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) + .map(SelectedRecipe::new) + .toList(); + } + return Util.getRandom(cache, level.getRandom()); + } + + @Override + public MapCodec getType() { + return CODEC; + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java new file mode 100644 index 000000000..2c65fa867 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java @@ -0,0 +1,31 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.mojang.datafixers.util.Either; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; + +import java.util.List; + +public record SelectedRecipe(ResourceLocation id, Either recipe) { + public SelectedRecipe(RecipeHolder holder) { + this(holder.id(), holder.value() instanceof ShapedRecipe sr ? Either.left(sr) : Either.right((ShapelessRecipe) holder.value())); + } + + public SelectedRecipe(ResourceLocation id, RecipeManager manager) { + this(manager.byKey(id).orElseThrow()); + } + + public ItemStack getResult(RegistryAccess access) { + return recipe.map(rp -> rp.getResultItem(access), rp -> rp.getResultItem(access)); + } + + public List decompose() { + return recipe.map(ShapedRecipe::getIngredients, ShapelessRecipe::getIngredients); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java new file mode 100644 index 000000000..b52b889a7 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java @@ -0,0 +1,53 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.lovetropics.minigames.common.content.crafting_bee.SelectedRecipe; +import com.mojang.serialization.MapCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public class FromRecipeDecomposer implements IngredientDecomposer { + public static final MapCodec CODEC = ResourceLocation.CODEC.listOf() + .fieldOf("recipes").xmap(FromRecipeDecomposer::new, d -> d.recipes); + + private final List recipes; + + private final Map> cache = new IdentityHashMap<>(); + + public FromRecipeDecomposer(List recipes) { + this.recipes = recipes; + } + + @Override + public @Nullable List decompose(Ingredient ingredient) { + if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.ItemValue(ItemStack item)) { + return cache.get(item.getItem()); + } + return null; + } + + @Override + public void prepareCache(ServerLevel level) { + cache.clear(); + + for (ResourceLocation recipe : recipes) { + level.getServer().getRecipeManager().byKey(recipe).map(SelectedRecipe::new) + .ifPresent(r -> cache.put( + r.getResult(level.registryAccess()).getItem(), + r.decompose() + )); + } + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java new file mode 100644 index 000000000..a34a341a3 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java @@ -0,0 +1,25 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface IngredientDecomposer { + BiMap> TYPES = ImmutableBiMap.of("prefer_from_tag", PreferItemFromTagDecomposer.CODEC, + "from_recipe", FromRecipeDecomposer.CODEC, + "simple_tag", SimpleTagToItemDecomposer.CODEC); + MapCodec CODEC = Codec.STRING.dispatchMap(s -> TYPES.inverse().get(s.codec()), TYPES::get); + + @Nullable + List decompose(Ingredient ingredient); + + default void prepareCache(ServerLevel level) {} + + MapCodec codec(); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java new file mode 100644 index 000000000..ad2e61605 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java @@ -0,0 +1,40 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public record PreferItemFromTagDecomposer(Map, Item> preferences) implements IngredientDecomposer { + public PreferItemFromTagDecomposer(Map, Item> preferences) { + this.preferences = new IdentityHashMap<>(preferences); + } + + public static final MapCodec CODEC = Codec.unboundedMap( + TagKey.hashedCodec(Registries.ITEM), BuiltInRegistries.ITEM.byNameCodec() + ).fieldOf("preferences").xmap(PreferItemFromTagDecomposer::new, PreferItemFromTagDecomposer::preferences); + + @Override + public @Nullable List decompose(Ingredient ingredient) { + if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.TagValue(TagKey tag)) { + var pref = preferences.get(tag); + if (pref != null) { + return List.of(Ingredient.of(pref)); + } + } + return null; + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java new file mode 100644 index 000000000..a5856b4ef --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java @@ -0,0 +1,32 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.mojang.serialization.MapCodec; +import net.minecraft.world.item.crafting.Ingredient; +import net.neoforged.neoforge.common.crafting.DifferenceIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public record SimpleTagToItemDecomposer() implements IngredientDecomposer { + public static final MapCodec CODEC = MapCodec.unit(SimpleTagToItemDecomposer::new); + + @Override + public @Nullable List decompose(Ingredient ingredient) { + // This is a "hack". Neo will sometimes replace a vanilla recipe with a difference ingredient (#chests - #chests/trapped) + // we just resolve it and return the first item + if (ingredient.getCustomIngredient() != null) { + return List.of(Ingredient.of(ingredient.getItems()[0])); + } else if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.TagValue) { + var items = ingredient.getValues()[0].getItems(); + if (items.size() == 1) { + return items.stream().map(Ingredient::of).toList(); + } + } + return null; + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java new file mode 100644 index 000000000..cdfbe9679 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java @@ -0,0 +1,87 @@ +package com.lovetropics.minigames.common.content.river_race; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.client.CustomItemRenderers; +import com.lovetropics.minigames.common.content.river_race.behaviour.*; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlock; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import com.lovetropics.minigames.common.content.river_race.block.TriviaChestBlock; +import com.lovetropics.minigames.common.content.river_race.block.TriviaChestBlockEntity; +import com.lovetropics.minigames.common.util.registry.GameBehaviorEntry; +import com.lovetropics.minigames.common.util.registry.LoveTropicsRegistrate; +import com.tterrag.registrate.Registrate; +import com.tterrag.registrate.providers.ProviderType; +import com.tterrag.registrate.util.entry.BlockEntityEntry; +import com.tterrag.registrate.util.entry.BlockEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; + +public class RiverRace { + private static final LoveTropicsRegistrate REGISTRATE = LoveTropics.registrate(); + public static final GameBehaviorEntry TRIVIA_BEHAVIOUR = REGISTRATE.object("trivia").behavior(TriviaBehaviour.CODEC).register(); + public static final GameBehaviorEntry START_MICROGAMES_ACTION = REGISTRATE.object("start_microgames").behavior(StartMicrogamesAction.CODEC).register(); + public static final GameBehaviorEntry VICTORY_POINTS_BEHAVIOR = REGISTRATE.object("victory_points").behavior(VictoryPointsBehavior.CODEC).register(); + public static final GameBehaviorEntry RIVER_RACE_MERCHANT_BEHAVIOR = REGISTRATE.object("river_race_merchant").behavior(RiverRaceMerchantBehavior.CODEC).register(); + public static final GameBehaviorEntry RIVER_RACE_PROGRESS_BEHAVIOUR = REGISTRATE.object("river_race_progress").behavior(ProgressBehaviour.CODEC).register(); + + public static final BlockEntry TRIVIA_GATE = REGISTRATE + .block("trivia_gate", TriviaBlock.GateTriviaBlock::new) + .initialProperties(() -> Blocks.STONE) + .properties(BlockBehaviour.Properties::noLootTable) + .blockEntity(TriviaBlockEntity::new) + .build() + .blockstate((ctx, prox) -> { + prox.simpleBlock(ctx.get()); + }) + .simpleItem() + .register(); + public static final BlockEntry TRIVIA_COLLECTABLE = REGISTRATE + .block("trivia_collectable", TriviaBlock.CollectableTriviaBlock::new) + .initialProperties(() -> Blocks.STONE) + .properties(BlockBehaviour.Properties::noLootTable) + .blockEntity(TriviaBlockEntity::new) + .build() + .blockstate((ctx, prox) -> { + prox.simpleBlock(ctx.get()); + }) + .simpleItem() + .register(); + public static final BlockEntry TRIVIA_REWARD = REGISTRATE + .block("trivia_reward", TriviaBlock.RewardTriviaBlock::new) + .initialProperties(() -> Blocks.STONE) + .properties(BlockBehaviour.Properties::noLootTable) + .blockEntity(TriviaBlockEntity::new) + .build() + .blockstate((ctx, prox) -> { + prox.simpleBlock(ctx.get()); + }) + .simpleItem() + .register(); + + public static final BlockEntry TRIVIA_CHEST = REGISTRATE + .block("trivia_chest", TriviaChestBlock::new) + .initialProperties(() -> Blocks.STONE) + .properties(BlockBehaviour.Properties::noLootTable) + .blockstate((ctx, prov) -> prov.simpleBlock(ctx.get(), prov.models().getBuilder(ctx.getName()).texture("particle", prov.modLoc("block/trivia_reward")))) + .blockEntity(TriviaChestBlockEntity::new) + .build() + .item() + .clientExtension(() -> CustomItemRenderers::triviaChestItem) + .model((ctx, prov) -> prov.withExistingParent(ctx.getName(), "item/chest") + .texture("particle", prov.modLoc("block/trivia_reward"))) + .build() + .addMiscData(ProviderType.LANG, prov -> { + prov.add(LoveTropics.ID + ".container.triviaChest", "Trivia Chest"); + prov.add(LoveTropics.ID + ".container.triviaChestDouble", "Large Trivia Chest"); + }) + .register(); + + public static final BlockEntityEntry TRIVIA_BLOCK_ENTITY = BlockEntityEntry.cast(REGISTRATE.get("trivia_gate", Registries.BLOCK_ENTITY_TYPE)); + public static final BlockEntityEntry TRIVIA_CHEST_BLOCK_ENTITY = BlockEntityEntry.cast(REGISTRATE.get("trivia_chest", Registries.BLOCK_ENTITY_TYPE)); + + + public static void init() { + } + +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java new file mode 100644 index 000000000..a93e62e57 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java @@ -0,0 +1,23 @@ +package com.lovetropics.minigames.common.content.river_race; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.core.game.util.TranslationCollector; +import net.minecraft.network.chat.Component; + +import java.util.function.BiConsumer; + +public final class RiverRaceTexts { + private static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.river_race."); + + public static final Component SHOP = KEYS.add("shop", "Shop"); + + static { + + } + + public static void collectTranslations(BiConsumer consumer) { + KEYS.forEach(consumer); + + consumer.accept(LoveTropics.ID + ".minigame.river_race", "River Race"); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/TriviaEvents.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/TriviaEvents.java new file mode 100644 index 000000000..986e5d44d --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/TriviaEvents.java @@ -0,0 +1,32 @@ +package com.lovetropics.minigames.common.content.river_race; + +import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; +import com.lovetropics.minigames.common.content.river_race.block.HasTrivia; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import com.lovetropics.minigames.common.core.game.behavior.event.GameEventType; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +public class TriviaEvents { + + public static final GameEventType ANSWER_TRIVIA_BLOCK_QUESTION = GameEventType.create(AnswerQuestion.class, + listeners -> (player, world, pos, triviaBlockEntity, question, answer) -> { + for (AnswerQuestion listener : listeners) { + boolean isCorrect = listener.onAnswerQuestion(player, world, pos, triviaBlockEntity, question, answer); + if (isCorrect) { + return true; + } + } + return false; + }); + + + public interface AnswerQuestion { + boolean onAnswerQuestion(ServerPlayer player, ServerLevel world, BlockPos pos, + HasTrivia triviaBlockEntity, + @Nullable TriviaBehaviour.TriviaQuestion question, String answer); + } + +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/ProgressBehaviour.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/ProgressBehaviour.java new file mode 100644 index 000000000..8fe73320b --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/ProgressBehaviour.java @@ -0,0 +1,68 @@ +package com.lovetropics.minigames.common.content.river_race.behaviour; + +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.state.team.GameTeam; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import com.lovetropics.minigames.common.core.game.util.GameBossBar; +import com.lovetropics.minigames.common.core.game.util.GlobalGameWidgets; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.BossEvent; +import net.minecraft.world.item.DyeColor; + +import java.util.HashMap; +import java.util.Map; + +public class ProgressBehaviour implements IGameBehavior { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.INT.optionalFieldOf("max_points", 1).forGetter(c -> c.maxVictoryPoints) + ).apply(i, ProgressBehaviour::new)); + private final int maxVictoryPoints; + private final Map teamBars = new HashMap<>(); + + public ProgressBehaviour(int maxVictoryPoints) { + this.maxVictoryPoints = maxVictoryPoints; + } + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + GlobalGameWidgets widgets = GlobalGameWidgets.registerTo(game, events); + TeamState teams = game.instanceState().getOrThrow(TeamState.KEY); + for (GameTeamKey teamKey : teams.getTeamKeys()) { + GameTeam teamByKey = teams.getTeamByKey(teamKey); + if(teamByKey == null){ + continue; + } + GameBossBar bossBar = widgets.openBossBar(teamByKey.config().styledName(), getTeamColour(teamByKey.config().dye()), BossEvent.BossBarOverlay.NOTCHED_20); + bossBar.setProgress(0); + teamBars.put(teamKey, bossBar); + } + events.listen(RiverRaceEvents.VICTORY_POINTS_CHANGED, (team, value, lastValue) -> { + calculateTeamProgress(team, value); + }); + } + + public void calculateTeamProgress(GameTeamKey team, int victoryPoints){ + GameBossBar gameBossBar = teamBars.get(team); + if(gameBossBar != null){ + gameBossBar.setProgress((float) victoryPoints / maxVictoryPoints); + } + } + + private BossEvent.BossBarColor getTeamColour(DyeColor dyeColor){ + return switch (dyeColor){ + case BLUE -> BossEvent.BossBarColor.BLUE; + case RED -> BossEvent.BossBarColor.RED; + default -> BossEvent.BossBarColor.WHITE; + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java new file mode 100644 index 000000000..102452f8d --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java @@ -0,0 +1,187 @@ +package com.lovetropics.minigames.common.content.river_race.behaviour; + +import com.lovetropics.lib.BlockBox; +import com.lovetropics.lib.codec.MoreCodecs; +import com.lovetropics.minigames.common.content.biodiversity_blitz.BiodiversityBlitzTexts; +import com.lovetropics.minigames.common.content.biodiversity_blitz.merchant.BbMerchant; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +// TODO Merge with BbMerchantBehavior? +public class RiverRaceMerchantBehavior implements IGameBehavior { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.STRING.fieldOf("zone").forGetter(c -> c.zone), + BuiltInRegistries.ENTITY_TYPE.byNameCodec().fieldOf("entity").forGetter(c -> c.entity), + ComponentSerialization.CODEC.optionalFieldOf("name", CommonComponents.EMPTY).forGetter(c -> c.name), + Offer.CODEC.listOf().fieldOf("offers").forGetter(c -> c.offers) + ).apply(i, RiverRaceMerchantBehavior::new)); + + private final String zone; + private final EntityType entity; + private final Component name; + private final List offers; + + private final Set merchants = new ObjectOpenHashSet<>(); + + private IGamePhase game; + + public RiverRaceMerchantBehavior(String zone, EntityType entity, Component name, List offers) { + this.zone = zone; + this.entity = entity; + this.name = name; + this.offers = offers; + } + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + this.game = game; + + events.listen(GamePhaseEvents.CREATE, this::onGameStarted); + events.listen(GamePlayerEvents.INTERACT_ENTITY, this::interactWithEntity); + } + + /** + * When the game loads, load this merchant into its proper section + */ + private void onGameStarted() { + ServerLevel world = game.level(); + + BlockBox region = game.mapRegions().getOrThrow(zone); + // if (region == null) return; + + Vec3 center = region.center(); + + Entity merchant = createMerchant(world); + if (merchant == null) return; + +// Direction direction = Util.getDirectionBetween(region, plot.spawn); +// float yaw = direction.toYRot(); +// + merchant.moveTo(center.x(), center.y() - 0.5, center.z(), 0 /*yaw*/, 0); +// merchant.setYHeadRot(yaw); + + world.getChunk(region.centerBlock()); + world.addFreshEntity(merchant); + + if (merchant instanceof Mob mob) { + mob.finalizeSpawn(world, world.getCurrentDifficultyAt(BlockPos.containing(center)), MobSpawnType.MOB_SUMMONED, null); + mob.setNoAi(true); + mob.setBaby(false); + mob.setInvulnerable(true); + } + + merchants.add(merchant.getUUID()); + } + + private InteractionResult interactWithEntity(ServerPlayer player, Entity target, InteractionHand hand) { + if (merchants.contains(target.getUUID())) { + MerchantOffers builtOffers = new MerchantOffers(); + for (Offer offer : offers) { + builtOffers.add(offer.build(game)); + } + + // TODO need a different screen? + BbMerchant merchant = new BbMerchant(player, builtOffers); + merchant.openTradingScreen(player, BiodiversityBlitzTexts.TRADING, 1); + + return InteractionResult.SUCCESS; + } + + return InteractionResult.PASS; + } + + @Nullable + private Entity createMerchant(ServerLevel world) { + Entity merchant = entity.create(world); + if (merchant != null) { + if (name != CommonComponents.EMPTY) { + merchant.setCustomName(name); + merchant.setCustomNameVisible(true); + } + + merchant.setSilent(true); + + return merchant; + } + + return null; + } + + public static final class Offer { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ItemCost.CODEC.fieldOf("input").forGetter(c -> c.input), + Output.CODEC.fieldOf("output").forGetter(c -> c.output) + ).apply(instance, Offer::new)); + + private final ItemCost input; + private final Output output; + + public Offer(ItemCost input, Output output) { + this.input = input; + this.output = output; + } + + public MerchantOffer build(IGamePhase game) { + return new MerchantOffer( + input, output.build(game), + Integer.MAX_VALUE, + 0, + 0 + ); + } + } + + public static final class Output { + private static final Codec CODEC = MoreCodecs.ITEM_STACK.xmap(Output::item, output -> output.item); + + @Nullable + private final ItemStack item; + + private Output(@Nullable ItemStack item) { + this.item = item; + } + + private static Output item(ItemStack item) { + return new Output(item); + } + + private ItemStack build(IGamePhase game) { + if (item != null) { + return item; + } + throw new IllegalStateException(); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/StartMicrogamesAction.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/StartMicrogamesAction.java new file mode 100644 index 000000000..75fdfe269 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/StartMicrogamesAction.java @@ -0,0 +1,50 @@ +package com.lovetropics.minigames.common.content.river_race.behaviour; + +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameActionEvents; +import com.lovetropics.minigames.common.core.game.impl.MultiGamePhase; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ExtraCodecs; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public record StartMicrogamesAction(List gameConfigs, int gamesPerRound) implements IGameBehavior { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.nonEmptyList(ResourceLocation.CODEC.listOf()).fieldOf("games").forGetter(StartMicrogamesAction::gameConfigs), + Codec.INT.optionalFieldOf("games_per_round", 1).forGetter(c -> c.gamesPerRound) + ).apply(i, StartMicrogamesAction::new)); + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + events.listen(GameActionEvents.APPLY, context -> { + queueMicrogames(game); + startQueuedMicrogame(game); + return true; + }); + } + + public void queueMicrogames(IGamePhase game) { + if (game instanceof MultiGamePhase multiGamePhase) { + multiGamePhase.clearQueuedGames(); + + final List configs = new ArrayList<>(gameConfigs); + Collections.shuffle(configs); + multiGamePhase.queueGames(configs.subList(0, gamesPerRound)); + } + } + + public void startQueuedMicrogame(final IGamePhase game) { + if (game instanceof MultiGamePhase multiGamePhase) { + multiGamePhase.startNextQueuedMicrogame(true); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java new file mode 100644 index 000000000..7822ec9af --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java @@ -0,0 +1,254 @@ +package com.lovetropics.minigames.common.content.river_race.behaviour; + +import com.lovetropics.lib.BlockBox; +import com.lovetropics.minigames.common.content.river_race.TriviaEvents; +import com.lovetropics.minigames.common.content.river_race.block.HasTrivia; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlock; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.IGameManager; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.util.GameScheduler; +import com.lovetropics.minigames.common.core.network.trivia.ShowTriviaMessage; +import com.lovetropics.minigames.common.core.network.trivia.TriviaAnswerResponseMessage; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.netty.buffer.ByteBuf; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.timers.TimerCallback; +import net.minecraft.world.level.timers.TimerQueue; +import net.minecraft.world.phys.BlockHitResult; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class TriviaBehaviour implements IGameBehavior { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.nonEmptyList(TriviaZone.CODEC.listOf()).fieldOf("zones").forGetter(TriviaBehaviour::zones), + Codec.INT.optionalFieldOf("question_lockout", 30).forGetter(TriviaBehaviour::questionLockout) + ).apply(i, TriviaBehaviour::new)); + private final List zones; + private final int questionLockout; + private final GameScheduler scheduler = new GameScheduler(); + private final Map lockedOutTriviaBlocks = new ConcurrentHashMap<>(); + + public TriviaBehaviour(List zones, int questionLockout) { + this.zones = zones; + this.questionLockout = questionLockout; + } + + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + events.listen(GamePhaseEvents.TICK, () -> { + scheduler.tick(); + Set longs = lockedOutTriviaBlocks.keySet(); + for (Long l : longs) { + if(System.currentTimeMillis() >= l){ + BlockPos blockPos = lockedOutTriviaBlocks.get(l); + BlockEntity blockEntity = game.level().getBlockEntity(blockPos); + if(blockEntity instanceof TriviaBlockEntity triviaBlockEntity){ + triviaBlockEntity.unlock(); + lockedOutTriviaBlocks.remove(l); + } + } + } + }); + events.listen(GamePlayerEvents.USE_BLOCK, (ServerPlayer player, ServerLevel world, + BlockPos pos, InteractionHand hand, BlockHitResult traceResult) -> { + if (hand == InteractionHand.OFF_HAND) { + return InteractionResult.PASS; + } + if (world.getBlockEntity(pos) instanceof HasTrivia hasTrivia && !hasTrivia.getState().isAnswered()) { + if (!hasTrivia.hasQuestion()) { + String inRegion = null; + for (String region : game.mapRegions().keySet()) { + if (region.startsWith("zone_")) { + if (game.mapRegions().getOrThrow(region).contains(pos)) { + inRegion = region; + break; + } + } + } + if (inRegion != null) { + int zone = Integer.parseInt(inRegion.split("_")[1]); + Optional first = zones.stream().filter(triviaZone -> triviaZone.zone == zone).findFirst(); + if (first.isPresent()) { + //TODO: Prevent this selecting questions that are already in use.. maybe some kind of id? + TriviaZone triviaZone = first.get(); + List filteredDifficultyList = triviaZone.questionPool.stream() + .filter(question -> question.difficulty() + .equalsIgnoreCase(hasTrivia.getTriviaType().difficulty())) + .toList(); + //Bad... + if (!filteredDifficultyList.isEmpty()) { + TriviaQuestion question = filteredDifficultyList.get(new Random().nextInt(filteredDifficultyList.size())); + hasTrivia.setQuestion(question); + } else { + player.sendSystemMessage(Component.literal("Failed to pick a question from the question pool for this trivia block").withStyle(ChatFormatting.RED)); + } + } + } + } + if (hasTrivia.getQuestion() != null) { + PacketDistributor.sendToPlayer(player, new ShowTriviaMessage(pos, hasTrivia.getQuestion(), hasTrivia.getState())); + } + return InteractionResult.SUCCESS_NO_ITEM_USED; + } + return InteractionResult.PASS; + }); + events.listen(TriviaEvents.ANSWER_TRIVIA_BLOCK_QUESTION, (ServerPlayer player, ServerLevel world, + BlockPos pos, + HasTrivia triviaBlockEntity, + TriviaQuestion question, String answer) -> { + if (question != null && !triviaBlockEntity.getState().lockedOut()) { + TriviaQuestion.TriviaQuestionAnswer answerObj = question.getAnswer(answer); + if (answerObj != null) { + if (answerObj.correct()) { + //TODO: Make this a translation key + player.sendSystemMessage(Component. + literal("Correct! Do something here!") + .withStyle(ChatFormatting.GREEN)); + if(triviaBlockEntity.getTriviaType() == TriviaBlock.TriviaType.GATE){ + //TODO: Open gate + Block blockType = null; + findNeighboursOfTypeAndDestroy(scheduler, world, pos, blockType); + } + triviaBlockEntity.markAsCorrect(); + PacketDistributor.sendToPlayer(player, new TriviaAnswerResponseMessage(pos, triviaBlockEntity.getState())); + } else { + //TODO: Make this a translation key + player.sendSystemMessage(Component. + literal("Incorrect! This question is now locked out for " + questionLockout() + " seconds!") + .withStyle(ChatFormatting.RED)); + lockedOutTriviaBlocks.put(triviaBlockEntity.lockout(questionLockout()), pos); + PacketDistributor.sendToPlayer(player, new TriviaAnswerResponseMessage(pos, triviaBlockEntity.getState())); + } + if (player instanceof final ServerPlayer serverPlayer) { + game.invoker(RiverRaceEvents.ANSWER_QUESTION).onAnswer(serverPlayer, triviaBlockEntity.getTriviaType(), answerObj.correct()); + } + return answerObj.correct(); + } + } + return false; + }); + } + + private static void findNeighboursOfTypeAndDestroy(GameScheduler scheduler, ServerLevel world, BlockPos pos, Block blockType) { + for (Direction direction : Direction.values()) { + BlockPos relative = pos.relative(direction); + BlockState blockState = world.getBlockState(relative); + if(!blockState.isAir()){ + if(blockType == null){ + blockType = blockState.getBlock(); + } + if(blockState.is(blockType)){ + world.destroyBlock(relative, false); + Block finalBlockType = blockType; + scheduler.delayedTickEvent("gateDestroy" + relative, () -> { + findNeighboursOfTypeAndDestroy(scheduler, world, relative, finalBlockType); + }, 10); + } + } + } + } + + public List zones() { + return zones; + } + + public int questionLockout() { + return questionLockout; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (TriviaBehaviour) obj; + return Objects.equals(this.zones, that.zones) && + this.questionLockout == that.questionLockout; + } + + @Override + public int hashCode() { + return Objects.hash(zones, questionLockout); + } + + @Override + public String toString() { + return "TriviaBehaviour[" + + "zones=" + zones + ", " + + "questionLockout=" + questionLockout + ']'; + } + + + public record TriviaZone(int zone, List questionPool) { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + Codec.INT.fieldOf("zone_id").forGetter(TriviaZone::zone), + ExtraCodecs.nonEmptyList(TriviaQuestion.CODEC.listOf()).fieldOf("questions").forGetter(TriviaZone::questionPool) + ).apply(i, TriviaZone::new)); + } + + public record TriviaQuestion(String question, List answers, String difficulty) { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + Codec.STRING.fieldOf("question").forGetter(TriviaQuestion::question), + ExtraCodecs.nonEmptyList(TriviaQuestionAnswer.CODEC.listOf()).fieldOf("answers").forGetter(TriviaQuestion::answers), + Codec.STRING.fieldOf("difficulty").forGetter(TriviaQuestion::difficulty) + ).apply(i, TriviaQuestion::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, TriviaQuestion::question, + TriviaQuestionAnswer.STREAM_CODEC.apply(ByteBufCodecs.list()), TriviaQuestion::answers, + ByteBufCodecs.STRING_UTF8, TriviaQuestion::difficulty, + TriviaQuestion::new + ); + + public record TriviaQuestionAnswer(String text, boolean correct) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, TriviaQuestionAnswer::text, + ByteBufCodecs.BOOL, TriviaQuestionAnswer::correct, + TriviaQuestionAnswer::new + ); + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + Codec.STRING.fieldOf("text").forGetter(TriviaQuestionAnswer::text), + Codec.BOOL.fieldOf("correct").forGetter(TriviaQuestionAnswer::correct) + ).apply(i, TriviaQuestionAnswer::new)); + } + + + @Nullable + public TriviaBehaviour.TriviaQuestion.TriviaQuestionAnswer getAnswer(String text) { + for (TriviaQuestionAnswer answer : answers) { + if (answer.text().equalsIgnoreCase(text)) { + return answer; + } + } + return null; + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java new file mode 100644 index 000000000..382e81ef1 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java @@ -0,0 +1,121 @@ +package com.lovetropics.minigames.common.content.river_race.behaviour; + +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlock; +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.content.river_race.state.RiverRaceState; +import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsGameState; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.state.team.GameTeam; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class VictoryPointsBehavior implements IGameBehavior { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.INT.optionalFieldOf("points_per_question", 1).forGetter(c -> c.pointsPerQuestion), + Codec.INT.optionalFieldOf("points_per_block_collected", 1).forGetter(c -> c.pointsPerBlockCollected), + Codec.INT.optionalFieldOf("points_per_game_won", 1).forGetter(c -> c.pointsPerGameWon) + ).apply(i, VictoryPointsBehavior::new)); + + private IGamePhase game; + + private final int pointsPerQuestion; + private final int pointsPerBlockCollected; + private final int pointsPerGameWon; + + public VictoryPointsBehavior(final int pointsPerQuestion, final int pointsPerBlockCollected, final int pointsPerGameWon) { + this.pointsPerQuestion = pointsPerQuestion; + this.pointsPerBlockCollected = pointsPerBlockCollected; + this.pointsPerGameWon = pointsPerGameWon; + } + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + this.game = game; + + // Victory points from trivia + events.listen(RiverRaceEvents.ANSWER_QUESTION, this::onQuestionAnswered); + // Victory points from collectible blocks + events.listen(GamePlayerEvents.BREAK_BLOCK, this::onBlockBroken); + // Victory points from winning microgame + events.listen(GameLogicEvents.WIN_TRIGGERED, this::onWinTriggered); + } + + private void onWinTriggered(Component component) { + for (final ServerPlayer player : game.participants()) { + if (Objects.equals(player.getDisplayName(), component)) { + tryAddPoints(player, pointsPerGameWon); + player.displayClientMessage(Component.literal("YOU WIN!!!! Victory points for team: " + getPoints(player)), false); + return; + } + } + + var teams = game.instanceState().getOrNull(TeamState.KEY); + if (teams == null) return; + for (final GameTeam team : teams) { + if (Objects.equals(team.config().name(), component)) { + tryAddPoints(team.key(), pointsPerGameWon); + } + } + } + + private void tryAddPoints(final ServerPlayer player, final int points) { + final VictoryPointsGameState pointState = state(); + if (pointState != null) { + pointState.addPointsToTeam(player, points); + } + } + + private void tryAddPoints(final GameTeamKey team, final int points) { + final VictoryPointsGameState pointState = state(); + if (pointState != null) { + pointState.addPointsToTeam(team, points); + } + } + + private int getPoints(final ServerPlayer player) { + final VictoryPointsGameState gameState = state(); + if (gameState != null) { + return gameState.getVictoryPoints(player); + } + return -1; + } + + @Nullable + private VictoryPointsGameState state() { + // TODO how to make this more generic to not be specific to river race? + return game.state().getOrNull(RiverRaceState.KEY); + } + + private InteractionResult onBlockBroken(ServerPlayer serverPlayer, BlockPos blockPos, BlockState blockState, InteractionHand interactionHand) { + return InteractionResult.PASS; + } + + private void onQuestionAnswered(ServerPlayer player, TriviaBlock.TriviaType triviaType, boolean correct) { + if (correct) { + if(triviaType == TriviaBlock.TriviaType.COLLECTABLE) { + tryAddPoints(player, pointsPerQuestion); + } + player.displayClientMessage(Component.literal("CORRECT! Victory points for team: " + getPoints(player)), false); + } else { + player.displayClientMessage(Component.literal("WRONG >:( Victory points for team: " + getPoints(player)), false); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/block/HasTrivia.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/HasTrivia.java new file mode 100644 index 000000000..b9d155bcf --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/HasTrivia.java @@ -0,0 +1,22 @@ +package com.lovetropics.minigames.common.content.river_race.block; + +import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; + +public interface HasTrivia { + + boolean hasQuestion(); + + void setQuestion(TriviaBehaviour.TriviaQuestion question); + + TriviaBehaviour.TriviaQuestion getQuestion(); + + TriviaBlock.TriviaType getTriviaType(); + + long lockout(int lockoutSeconds); + + void unlock(); + + void markAsCorrect(); + + TriviaBlockEntity.TriviaBlockState getState(); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaBlock.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaBlock.java new file mode 100644 index 000000000..b9bf51b99 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaBlock.java @@ -0,0 +1,76 @@ +package com.lovetropics.minigames.common.content.river_race.block; + +import com.lovetropics.minigames.common.content.river_race.RiverRace; +import net.minecraft.core.BlockPos; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import org.jetbrains.annotations.Nullable; + +public class TriviaBlock extends Block implements EntityBlock { + + public static class RewardTriviaBlock extends TriviaBlock { + public RewardTriviaBlock(Properties properties) { + super(properties, TriviaType.REWARD); + } + } + public static class GateTriviaBlock extends TriviaBlock { + public GateTriviaBlock(Properties properties) { + super(properties, TriviaType.GATE); + } + } + + public static class CollectableTriviaBlock extends TriviaBlock { + public CollectableTriviaBlock(Properties properties) { + super(properties, TriviaType.COLLECTABLE); + } + } + + public enum TriviaType implements StringRepresentable { + REWARD("easy"), + GATE("medium"), + COLLECTABLE("hard"); + + private String difficulty; + TriviaType(String difficulty){ + this.difficulty = difficulty; + } + + public String difficulty() { + return difficulty; + } + + @Override + public String getSerializedName() { + return toString().toLowerCase(); + } + } + + private final TriviaType type; + + public TriviaBlock(Properties properties, TriviaType blockType) { + super(properties); + this.type = blockType; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new TriviaBlockEntity(RiverRace.TRIVIA_BLOCK_ENTITY.get(), blockPos, blockState) + .setTriviaType(type); + } + + public TriviaType getType() { + return type; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + } + + +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaBlockEntity.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaBlockEntity.java new file mode 100644 index 000000000..c3aa83b92 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaBlockEntity.java @@ -0,0 +1,165 @@ +package com.lovetropics.minigames.common.content.river_race.block; + +import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; +import com.mojang.logging.LogUtils; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.Connection; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; + +import java.util.Optional; + +public class TriviaBlockEntity extends BlockEntity implements HasTrivia { + + public record TriviaBlockState(boolean isAnswered, Optional correctAnswer, boolean lockedOut, long unlocksAt){ + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, TriviaBlockState::isAnswered, + ByteBufCodecs.optional(ByteBufCodecs.STRING_UTF8), TriviaBlockState::correctAnswer, + ByteBufCodecs.BOOL, TriviaBlockState::lockedOut, + ByteBufCodecs.VAR_LONG, TriviaBlockState::unlocksAt, + TriviaBlockState::new + ); + } + private static final Logger LOGGER = LogUtils.getLogger(); + public static final String TAG_QUESTION = "question"; + public static final String TAG_UNLOCKS_AT = "unlocksAt"; + public static final String TAG_ANSWERED = "answered"; + private TriviaBehaviour.TriviaQuestion question; + private long unlocksAt; + private boolean answered; + private TriviaBlock.TriviaType triviaType; + public TriviaBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + if(blockState.getBlock() instanceof TriviaBlock triviaBlock){ + this.triviaType = triviaBlock.getType(); + } + } + + public TriviaBlockEntity setTriviaType(TriviaBlock.TriviaType blockType){ + this.triviaType = blockType; + return this; + } + + public TriviaBlock.TriviaType getTriviaBlockType() { + return triviaType; + } + + @Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + if(question != null) { + tag.put(TAG_QUESTION, TriviaBehaviour.TriviaQuestion.CODEC.encodeStart(NbtOps.INSTANCE, question).getOrThrow()); + } + if(unlocksAt > 0){ + tag.putLong(TAG_UNLOCKS_AT, unlocksAt); + } + tag.putBoolean(TAG_ANSWERED, answered); + } + + @Override + public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + if(tag.contains(TAG_QUESTION)) { + TriviaBehaviour.TriviaQuestion.CODEC.parse(NbtOps.INSTANCE, tag.get(TAG_QUESTION)) + .resultOrPartial(LOGGER::error) + .ifPresent(q -> question = q); + } + if(tag.contains(TAG_UNLOCKS_AT)) { + unlocksAt = tag.getLong(TAG_UNLOCKS_AT); + } + if (tag.contains(TAG_ANSWERED)) { + answered = tag.getBoolean(TAG_ANSWERED); + } + } + + @Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + if(unlocksAt > 0) { + tag.putLong(TAG_UNLOCKS_AT, unlocksAt); + } + tag.putBoolean(TAG_ANSWERED, answered); + return tag; + } + + @Override + public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket pkt, HolderLookup.Provider registries) { + if (pkt.getTag() != null) { + handleUpdateTag(pkt.getTag(), registries); + } + } + + @Override + public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { + if (tag.contains(TAG_UNLOCKS_AT)) { + unlocksAt = tag.getLong(TAG_UNLOCKS_AT); + } + if (tag.contains(TAG_ANSWERED)) { + answered = tag.getBoolean(TAG_ANSWERED); + } + } + + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + private void markUpdated() { + setChanged(); + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), Block.UPDATE_ALL); + } + + public boolean hasQuestion(){ + return question != null; + } + + public void setQuestion(TriviaBehaviour.TriviaQuestion question) { + this.question = question; + markUpdated(); + } + + public TriviaBehaviour.TriviaQuestion getQuestion() { + return question; + } + + @Override + public TriviaBlock.TriviaType getTriviaType() { + return triviaType; + } + + public long lockout(int lockoutSeconds){ + unlocksAt = System.currentTimeMillis() + (lockoutSeconds * 1000L); + markUpdated(); + return unlocksAt; + } + public void unlock() { + unlocksAt = 0; + markUpdated(); + } + + public void markAsCorrect(){ + answered = true; + markUpdated(); + } + + public TriviaBlockState getState(){ + Optional correctAnswer = Optional.empty(); + if(answered){ + correctAnswer = Optional.of(question.answers().stream().filter(TriviaBehaviour.TriviaQuestion.TriviaQuestionAnswer::correct).findFirst().get().text()); + } + return new TriviaBlockState(answered, correctAnswer, unlocksAt > 0, unlocksAt); + } + +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaChestBlock.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaChestBlock.java new file mode 100644 index 000000000..06d59502f --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaChestBlock.java @@ -0,0 +1,17 @@ +package com.lovetropics.minigames.common.content.river_race.block; + +import com.lovetropics.minigames.common.content.river_race.RiverRace; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +public class TriviaChestBlock extends ChestBlock { + public TriviaChestBlock(Properties properties) { + super(properties, RiverRace.TRIVIA_CHEST_BLOCK_ENTITY::get); + } + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new TriviaChestBlockEntity(pos, state); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaChestBlockEntity.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaChestBlockEntity.java new file mode 100644 index 000000000..77d07607b --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/block/TriviaChestBlockEntity.java @@ -0,0 +1,126 @@ +package com.lovetropics.minigames.common.content.river_race.block; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.river_race.RiverRace; +import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; +import com.mojang.logging.LogUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; + +import java.util.Optional; + +public class TriviaChestBlockEntity extends ChestBlockEntity implements HasTrivia { + private static final Logger LOGGER = LogUtils.getLogger(); + private TriviaBehaviour.TriviaQuestion question; + private long unlocksAt; + private boolean answered; + + public TriviaChestBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + } + + public TriviaChestBlockEntity(BlockPos pos, BlockState blockState) { + this(RiverRace.TRIVIA_CHEST_BLOCK_ENTITY.get(), pos, blockState); + } + + + @Override + public Component getName() { + return Component.translatable(LoveTropics.ID + ".container.triviaChest"); + } + + @Override + protected Component getDefaultName() { + return getName(); + } + + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + if(tag.contains(TriviaBlockEntity.TAG_QUESTION)) { + TriviaBehaviour.TriviaQuestion.CODEC.parse(NbtOps.INSTANCE, tag.get(TriviaBlockEntity.TAG_QUESTION)) + .resultOrPartial(LOGGER::error) + .ifPresent(q -> question = q); + } + if(tag.contains(TriviaBlockEntity.TAG_UNLOCKS_AT)) { + unlocksAt = tag.getLong(TriviaBlockEntity.TAG_UNLOCKS_AT); + } + if (tag.contains(TriviaBlockEntity.TAG_ANSWERED)) { + answered = tag.getBoolean(TriviaBlockEntity.TAG_ANSWERED); + } + } + + @Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + if(question != null) { + tag.put(TriviaBlockEntity.TAG_QUESTION, TriviaBehaviour.TriviaQuestion.CODEC.encodeStart(NbtOps.INSTANCE, question).getOrThrow()); + } + if(unlocksAt > 0){ + tag.putLong(TriviaBlockEntity.TAG_UNLOCKS_AT, unlocksAt); + } + tag.putBoolean(TriviaBlockEntity.TAG_ANSWERED, answered); + } + + @Override + public boolean hasQuestion() { + return question != null; + } + private void markUpdated() { + setChanged(); + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), Block.UPDATE_ALL); + } + + @Override + public void setQuestion(TriviaBehaviour.TriviaQuestion question) { + this.question = question; + markUpdated(); + } + + @Override + public TriviaBehaviour.TriviaQuestion getQuestion() { + return question; + } + + @Override + public TriviaBlock.TriviaType getTriviaType() { + return TriviaBlock.TriviaType.REWARD; + } + + @Override + public long lockout(int lockoutSeconds) { + unlocksAt = System.currentTimeMillis() + (lockoutSeconds * 1000L); + markUpdated(); + return unlocksAt; + } + + @Override + public void unlock() { + unlocksAt = 0; + markUpdated(); + } + + @Override + public void markAsCorrect() { + answered = true; + markUpdated(); + } + + @Override + public TriviaBlockEntity.TriviaBlockState getState() { + Optional correctAnswer = Optional.empty(); + if(answered){ + correctAnswer = Optional.of(question.answers().stream().filter(TriviaBehaviour.TriviaQuestion.TriviaQuestionAnswer::correct).findFirst().get().text()); + } + return new TriviaBlockEntity.TriviaBlockState(answered, correctAnswer, unlocksAt > 0, unlocksAt); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java new file mode 100644 index 000000000..b07b30d01 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java @@ -0,0 +1,40 @@ +package com.lovetropics.minigames.common.content.river_race.event; + +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlock; +import com.lovetropics.minigames.common.core.game.behavior.event.GameEventType; +import com.lovetropics.minigames.common.core.game.impl.MultiGamePhase; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import net.minecraft.server.level.ServerPlayer; + +public class RiverRaceEvents { + + public static final GameEventType ANSWER_QUESTION = GameEventType.create(AnswerTriviaQuestion.class, listeners -> (player, triviaType, correct) -> { + for (AnswerTriviaQuestion listener : listeners) { + listener.onAnswer(player, triviaType, correct); + } + }); + + public static final GameEventType VICTORY_POINTS_CHANGED = GameEventType.create(VictoryPointsChanged.class, listeners -> (team, value, lastValue) -> { + for (VictoryPointsChanged listener : listeners) { + listener.onVictoryPointsChanged(team, value, lastValue); + } + }); + + public static final GameEventType MICROGAME_STARTED = GameEventType.create(MicrogameStarted.class, listeners -> (game) -> { + for (MicrogameStarted listener : listeners) { + listener.onMicrogameStarted(game); + } + }); + + public interface AnswerTriviaQuestion { + void onAnswer(ServerPlayer player, final TriviaBlock.TriviaType triviaType, final boolean correct); + } + + public interface VictoryPointsChanged { + void onVictoryPointsChanged(GameTeamKey team, int value, int lastValue); + } + + public interface MicrogameStarted { + void onMicrogameStarted(MultiGamePhase game); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/package-info.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/package-info.java new file mode 100644 index 000000000..4551f5968 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/package-info.java @@ -0,0 +1,9 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +package com.lovetropics.minigames.common.content.river_race; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java new file mode 100644 index 000000000..2290d66a8 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java @@ -0,0 +1,91 @@ +package com.lovetropics.minigames.common.content.river_race.state; + +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.state.GameStateKey; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; + +public class RiverRaceState implements VictoryPointsGameState { + public static final GameStateKey KEY = GameStateKey.create("RiverRace"); + + private final IGamePhase game; + + public RiverRaceState(IGamePhase game) { + this.game = game; + } + + @Override + public void addPointsToTeam(final GameTeamKey team, final int points) { + addToTeam(Trackers.POINTS, team, points); + } + + public void addCoinsToTeam(final GameTeamKey team, final int coins) { + addToTeam(Trackers.COINS, team, coins); + } + + @Override + public void addPointsToTeam(final ServerPlayer player, final int points) { + addPointsToTeam(getTeamForPlayer(player), points); + } + + @Override + public int getVictoryPoints(GameTeamKey team) { + return Trackers.POINTS.getTracker().getOrDefault(team, 0); + } + + public int getCoins(GameTeamKey team) { + return Trackers.COINS.getTracker().getOrDefault(team, 0); + } + + @Override + public int getVictoryPoints(ServerPlayer player) { + return getVictoryPoints(getTeamForPlayer(player)); + } + + public int getCoins(ServerPlayer player) { + return getCoins(getTeamForPlayer(player)); + } + + @Override + public void reset() { + for (final Trackers tracker : Trackers.values()) { + tracker.reset(); + } + } + + @Nullable + private GameTeamKey getTeamForPlayer(ServerPlayer player) { + TeamState teams = game.instanceState().getOrThrow(TeamState.KEY); + return teams.getTeamForPlayer(player); + } + + private void addToTeam(final Trackers type, final GameTeamKey team, final int amount) { + int lastValue = type.getTracker().addTo(team, amount); + game.invoker(RiverRaceEvents.VICTORY_POINTS_CHANGED).onVictoryPointsChanged(team, lastValue + amount, lastValue); + } + + public enum Trackers { + POINTS, + COINS; + + private final Object2IntOpenHashMap tracker; + + Trackers() { + tracker = new Object2IntOpenHashMap<>(); + tracker.defaultReturnValue(0); + } + + private Object2IntOpenHashMap getTracker() { + return tracker; + } + + private void reset() { + tracker.clear(); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java new file mode 100644 index 000000000..7bcf95e9c --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java @@ -0,0 +1,13 @@ +package com.lovetropics.minigames.common.content.river_race.state; + +import com.lovetropics.minigames.common.core.game.state.IGameState; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import net.minecraft.server.level.ServerPlayer; + +public interface VictoryPointsGameState extends IGameState { + void addPointsToTeam(final GameTeamKey team, final int points); + void addPointsToTeam(final ServerPlayer player, final int points); + int getVictoryPoints(GameTeamKey team); + int getVictoryPoints(ServerPlayer player); + void reset(); +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java b/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java index 0b8f1f0d6..90048898b 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java @@ -9,7 +9,6 @@ public enum GamePhaseType { PLAYING, // Players are loaded into and playing in a game world - PAUSED, // Game world is paused, but potentially child game worlds are now in PLAYING or WAITING WAITING, // Players are loaded into the 'waiting world' before being loaded into a game world ; diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/IGameDefinition.java b/src/main/java/com/lovetropics/minigames/common/core/game/IGameDefinition.java index 322817086..b31ef4377 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/IGameDefinition.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/IGameDefinition.java @@ -70,6 +70,8 @@ default int getMaximumParticipantCount() { return Integer.MAX_VALUE; } + default boolean isMultiGamePhase() {return false;} + IGamePhaseDefinition getPlayingPhase(); default Optional getWaitingPhase() { diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java index 8212d1c24..fe71d2507 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java @@ -66,6 +66,11 @@ default GameStateMap instanceState() { GameResult requestStop(GameStopReason reason); + /** + * Schedule a task to be run after the specified amount of seconds. + */ + void schedule(float seconds, Runnable task); + /** * Adds the player to this game instance with the given role, or if already in the change, changes their role. * The given player will be removed from their former role, if any. diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhaseDefinition.java b/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhaseDefinition.java index 4501bf74b..7313321ac 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhaseDefinition.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhaseDefinition.java @@ -16,4 +16,5 @@ default AABB getGameArea() { } BehaviorList createBehaviors(); + } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/PlayerIsolation.java b/src/main/java/com/lovetropics/minigames/common/core/game/PlayerIsolation.java index 6fca0bb92..c1e9c36b7 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/PlayerIsolation.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/PlayerIsolation.java @@ -1,10 +1,12 @@ package com.lovetropics.minigames.common.core.game; import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.core.game.impl.GameInstance; import com.lovetropics.minigames.common.util.LTGameTestFakePlayer; import com.mojang.serialization.Dynamic; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket; @@ -18,6 +20,7 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; +import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; @@ -42,6 +45,10 @@ public final class PlayerIsolation { private PlayerIsolation() { } + /** + * Player going into a GamePhase + * Saves player data and then teleports them + */ public ServerPlayer teleportTo(final ServerPlayer player, final ServerLevel newLevel, final Vec3 position, final float yRot, final float xRot) { final TransferableState transferableState = TransferableState.copyOf(player); return reloadPlayer(player, newPlayer -> { @@ -53,6 +60,9 @@ public ServerPlayer teleportTo(final ServerPlayer player, final ServerLevel newL }); } + /** + * Player is headed back to the main event world most likely + */ public ServerPlayer restore(final ServerPlayer player) { if (isIsolated(player)) { return reloadPlayerFromDisk(player); @@ -74,6 +84,29 @@ private ServerPlayer reloadPlayerFromDisk(final ServerPlayer player) { }); } + public ServerPlayer reloadPlayerFromMemory(final GameInstance game, final ServerPlayer player) { + return reloadPlayer(player, newPlayer -> { + final MinecraftServer server = player.getServer(); + final Optional playerTag = game.getPlayerStorage().fetchAndRemovePlayerData(player); + + final ResourceKey dimensionKey = playerTag.isPresent() ? getPlayerDimension(playerTag.get()) : Level.OVERWORLD; + final ServerLevel newLevel = Objects.requireNonNullElse(server.getLevel(dimensionKey), server.overworld()); + newPlayer.setServerLevel(newLevel); + + if (playerTag.isPresent()) { + final CompoundTag playerData = playerTag.get(); + setPlayerPosAndMotionFromTag(newPlayer, playerData); + + // TODO is this like super way too much? + // We just need to make sure we transfer things like inventory + newPlayer.readAdditionalSaveData(playerData); + + newPlayer.addTag(ISOLATED_TAG); + newPlayer.loadGameTypes(playerData); + } + }); + } + private ServerPlayer reloadPlayer(final ServerPlayer oldPlayer, final Consumer initializer) { if (oldPlayer instanceof LTGameTestFakePlayer) { initializer.accept(oldPlayer); @@ -86,6 +119,8 @@ private ServerPlayer reloadPlayer(final ServerPlayer oldPlayer, final Consumer 10.0 ? 0.0 : xPos, Math.abs(yPos) > 10.0 ? 0.0 : yPos, Math.abs(zPos) > 10.0 ? 0.0 : zPos); + double epsilon = 3.0000512E7; + newPlayer.setPosRaw(Mth.clamp(posTag.getDouble(0), -epsilon, epsilon), Mth.clamp(posTag.getDouble(1), -2.0E7, 2.0E7), Mth.clamp(posTag.getDouble(2), -3.0000512E7, 3.0000512E7)); + newPlayer.setYRot(rotationTag.getFloat(0)); + newPlayer.setXRot(rotationTag.getFloat(1)); + newPlayer.setOldPosAndRot(); + newPlayer.setYHeadRot(newPlayer.getYRot()); + newPlayer.setYBodyRot(newPlayer.getYRot()); + } + // State that can be transferred into isolation, but not back out private record TransferableState( ) { diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java index ffd8018f7..4a5578298 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java @@ -7,11 +7,14 @@ import net.minecraft.network.chat.PlayerChatMessage; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; @@ -205,6 +208,18 @@ public final class GamePlayerEvents { return false; }); + public static final GameEventType RETURN = GameEventType.create(Return.class, listeners -> (playerId, role) -> { + for (Return listener : listeners) { + listener.onReturn(playerId, role); + } + }); + + public static final GameEventType CRAFT = GameEventType.create(Craft.class, listeners -> (player, item, container) -> { + for (Craft listener : listeners) { + listener.onCraft(player, item, container); + } + }); + private GamePlayerEvents() { } @@ -287,4 +302,12 @@ public interface AllocateRoles { public interface Chat { boolean onChat(ServerPlayer player, PlayerChatMessage message); } + + public interface Return { + void onReturn(UUID playerId, @Nullable PlayerRole role); + } + + public interface Craft { + void onCraft(Player player, ItemStack crafted, Container craftingContainer); + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java index 0ab66d345..42fb2d1a5 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java @@ -6,6 +6,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.level.Explosion; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import javax.annotation.Nullable; @@ -42,6 +43,12 @@ public final class GameWorldEvents { } }); + public static final GameEventType BLOCK_LANDED = GameEventType.create(BlockLanded.class, listeners -> (level, pos, state) -> { + for (var listener : listeners) { + listener.onBlockLanded(level, pos, state); + } + }); + private GameWorldEvents() { } @@ -60,4 +67,8 @@ public interface SaplingGrow { public interface SetWeather { void onSetWeather(@Nullable WeatherEvent lastEvent, @Nullable WeatherEvent event); } + + public interface BlockLanded { + void onBlockLanded(Level level, BlockPos pos, BlockState state); + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java index f644f7327..910ba084b 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java @@ -3,6 +3,7 @@ import com.lovetropics.minigames.common.core.game.IGamePhase; import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; import com.lovetropics.minigames.common.core.game.client_state.GameClientState; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -15,5 +16,6 @@ public record SetGameClientStateBehavior(GameClientState state) implements IGame @Override public void register(IGamePhase game, EventRegistrar events) { GameClientState.applyGlobally(state, events); + events.listen(GamePhaseEvents.STOP, reason -> GameClientState.removeFromPlayers(state.getType(), game.allPlayers())); } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java index bb31cae5b..58bd0a849 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java @@ -2,14 +2,18 @@ import com.lovetropics.minigames.LoveTropics; import com.lovetropics.minigames.common.core.game.client_state.instance.BeaconClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.FogClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.GlowTeamMembersState; import com.lovetropics.minigames.common.core.game.client_state.instance.HealthTagClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.HideRecipeBookClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.InvertControlsClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.PointTagClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.ReplaceTexturesClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.ResourcePackClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.SidebarClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.SpectatingClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.SwapMovementClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.TeamMembersClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.TimeInterpolationClientState; import com.lovetropics.minigames.common.util.registry.GameClientTweakEntry; @@ -43,6 +47,10 @@ public final class GameClientStateTypes { public static final GameClientTweakEntry TEAM_MEMBERS = register("team_members", TeamMembersClientState.CODEC); public static final GameClientTweakEntry GLOW_TEAM_MEMBERS = register("glow_team_members", MapCodec.unit(GlowTeamMembersState.INSTANCE)); public static final GameClientTweakEntry POINT_TAGS = register("point_tags", PointTagClientState.CODEC); + public static final GameClientTweakEntry HIDE_RECIPE_BOOK = register("hide_recipe_book", HideRecipeBookClientState.CODEC); + public static final GameClientTweakEntry CRAFTING_BEE_CRAFTS = register("crafting_bee_crafts", CraftingBeeCraftsClientState.CODEC); + public static final GameClientTweakEntry INVERT_CONTROLS = register("invert_controls", InvertControlsClientState.CODEC); + public static final GameClientTweakEntry SWAP_MOVEMENT = register("swap_movement", SwapMovementClientState.CODEC); public static GameClientTweakEntry register(final String name, final MapCodec codec) { return REGISTRATE.object(name) diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCraftsClientState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCraftsClientState.java new file mode 100644 index 000000000..c53690080 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCraftsClientState.java @@ -0,0 +1,35 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.UUIDUtil; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import java.util.List; +import java.util.UUID; + +public record CraftingBeeCraftsClientState(List crafts, UUID gameId, int allowedHints) implements GameClientState { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Craft.CODEC.listOf().fieldOf("crafts").forGetter(CraftingBeeCraftsClientState::crafts), + UUIDUtil.CODEC.fieldOf("gameId").forGetter(CraftingBeeCraftsClientState::gameId), + Codec.INT.fieldOf("allowedHints").forGetter(CraftingBeeCraftsClientState::allowedHints) + ).apply(in, CraftingBeeCraftsClientState::new)); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.CRAFTING_BEE_CRAFTS.get(); + } + + public record Craft(ItemStack output, ResourceLocation recipeId, boolean done) { + public static final Codec CODEC = RecordCodecBuilder.create(in -> in.group( + ItemStack.CODEC.fieldOf("output").forGetter(Craft::output), + ResourceLocation.CODEC.fieldOf("recipe").forGetter(Craft::recipeId), + Codec.BOOL.fieldOf("done").forGetter(Craft::done) + ).apply(in, Craft::new)); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookClientState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookClientState.java new file mode 100644 index 000000000..16d5959fe --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookClientState.java @@ -0,0 +1,18 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; + +public record HideRecipeBookClientState(Component message) implements GameClientState { + public static final MapCodec CODEC = ComponentSerialization.CODEC + .fieldOf("message").xmap(HideRecipeBookClientState::new, HideRecipeBookClientState::message); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.HIDE_RECIPE_BOOK.get(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java new file mode 100644 index 000000000..1efe55742 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java @@ -0,0 +1,20 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public record InvertControlsClientState(boolean xAxis, boolean yAxis) implements GameClientState { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Codec.BOOL.optionalFieldOf("x_axis", false).forGetter(InvertControlsClientState::xAxis), + Codec.BOOL.optionalFieldOf("y_axis", true).forGetter(InvertControlsClientState::yAxis) + ).apply(in, InvertControlsClientState::new)); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.INVERT_CONTROLS.get(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java new file mode 100644 index 000000000..8947e0945 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java @@ -0,0 +1,15 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.MapCodec; + +public record SwapMovementClientState() implements GameClientState { + public static final MapCodec CODEC = MapCodec.unit(SwapMovementClientState::new); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.SWAP_MOVEMENT.get(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/config/GameConfig.java b/src/main/java/com/lovetropics/minigames/common/core/game/config/GameConfig.java index 336e1bd37..75ace219f 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/config/GameConfig.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/config/GameConfig.java @@ -29,6 +29,8 @@ public final class GameConfig implements IGameDefinition { public final GamePhaseConfig waiting; public final GamePhaseConfig playing; + public final boolean isMultiGame; + public GameConfig( ResourceLocation id, ResourceLocation backendId, @@ -39,7 +41,8 @@ public GameConfig( int minimumParticipants, int maximumParticipants, @Nullable GamePhaseConfig waiting, - GamePhaseConfig playing + GamePhaseConfig playing, + boolean isMultiGame ) { this.id = id; this.backendId = backendId; @@ -51,6 +54,7 @@ public GameConfig( this.maximumParticipants = maximumParticipants; this.waiting = waiting; this.playing = playing; + this.isMultiGame = isMultiGame; } public static Codec codec(ResourceLocation id) { @@ -63,14 +67,16 @@ public static Codec codec(ResourceLocation id) { Codec.INT.optionalFieldOf("minimum_participants", 1).forGetter(c -> c.minimumParticipants), Codec.INT.optionalFieldOf("maximum_participants", 100).forGetter(c -> c.maximumParticipants), GamePhaseConfig.CODEC.optionalFieldOf("waiting").forGetter(c -> Optional.ofNullable(c.waiting)), + Codec.BOOL.optionalFieldOf("is_multi_game").forGetter(c -> Optional.of(c.isMultiGame)), GamePhaseConfig.MAP_CODEC.forGetter(c -> c.playing) - ).apply(i, (backendIdOpt, statisticsKeyOpt, name, subtitleOpt, iconOpt, minimumParticipants, maximumParticipants, waitingOpt, active) -> { + ).apply(i, (backendIdOpt, statisticsKeyOpt, name, subtitleOpt, iconOpt, minimumParticipants, maximumParticipants, waitingOpt, is_multi_game, active) -> { ResourceLocation backendId = backendIdOpt.orElse(id); String statisticsKey = statisticsKeyOpt.orElse(id.getPath()); Component subtitle = subtitleOpt.orElse(null); + boolean isMultiGame = is_multi_game.orElse(false); ResourceLocation icon = iconOpt.orElse(null); GamePhaseConfig waiting = waitingOpt.orElse(null); - return new GameConfig(id, backendId, statisticsKey, name, subtitle, icon, minimumParticipants, maximumParticipants, waiting, active); + return new GameConfig(id, backendId, statisticsKey, name, subtitle, icon, minimumParticipants, maximumParticipants, waiting, active, isMultiGame); })); } @@ -125,4 +131,9 @@ public IGamePhaseDefinition getPlayingPhase() { public Optional getWaitingPhase() { return Optional.ofNullable(waiting); } + + @Override + public boolean isMultiGamePhase() { + return isMultiGame; + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/datagen/GameBuilder.java b/src/main/java/com/lovetropics/minigames/common/core/game/datagen/GameBuilder.java index 308a0f16c..346f1e4d4 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/datagen/GameBuilder.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/datagen/GameBuilder.java @@ -35,6 +35,7 @@ public class GameBuilder { private GamePhaseConfig waiting; @Nullable private GamePhaseConfig playing; + private boolean hasMultiGame = false; public GameBuilder(ResourceLocation id) { this.id = id; @@ -88,9 +89,14 @@ public GameBuilder withPlayingPhase(IGameMapProvider map, UnaryOperator state + // Useful for things that should retain state no matter how deep into microgames you go + private final Map multiPhaseDataMap = Maps.newHashMap(); + private boolean needsRolePrompt = false; private boolean closed; @@ -161,6 +169,23 @@ private GameResult onStateChange(LobbyStateManager.Change change) { return GameResult.ok(); } + public GameRewardsMap getRewardsMap() { + return rewardsMap; + } + + public Map getMultiPhaseDataMap() { + return multiPhaseDataMap; + } + + public IGameState createOrGetMultiPhaseState(final MultiGamePhase gamePhase) { + final ResourceLocation gameID = gamePhase.game.definition().getId(); + if (!multiPhaseDataMap.containsKey(gameID)) { + gamePhase.registerState(this); + } + return multiPhaseDataMap.get(gameID); + } + + // If old phase is null, it probably means we're entering from the main event world private GameResult onGamePhaseChange(@Nullable GamePhase oldPhase, @Nullable GamePhase newPhase) { GameResult result = GameResult.ok(); @@ -191,7 +216,11 @@ private GameResult onGamePhaseChange(@Nullable GamePhase oldPhase, @Nullab private GameResult startPhase(GamePhase phase) { phase.state().register(GameRewardsMap.STATE, rewardsMap); - return phase.start(); + if (phase instanceof final MultiGamePhase multiPhase) { + multiPhaseDataMap.clear(); + multiPhase.registerState(this); + } + return phase.start(false); } private void onGameInstanceChange(@Nullable GameInstance oldGame, @Nullable GameInstance newGame) { diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java index 49a770da6..f2e46855a 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java @@ -25,8 +25,10 @@ import com.lovetropics.minigames.common.core.game.util.GameTexts; import com.lovetropics.minigames.common.core.map.MapRegions; import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -36,6 +38,7 @@ import javax.annotation.Nullable; import java.util.Collections; import java.util.EnumMap; +import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.UUID; @@ -55,7 +58,7 @@ public class GamePhase implements IGamePhase { final GameStateMap phaseState = new GameStateMap(); final EnumMap roles = new EnumMap<>(PlayerRole.class); - private final Set addedPlayers = new ObjectArraySet<>(); + protected final Set addedPlayers = new ObjectArraySet<>(); final GameEventListeners events = new GameEventListeners(); @@ -64,7 +67,9 @@ public class GamePhase implements IGamePhase { GameStopReason stopped; boolean destroyed; - private GamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { + private final LinkedList pendingRunnables = new LinkedList<>(); + + protected GamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { this.game = game; server = game.server(); this.definition = definition; @@ -79,7 +84,7 @@ private GamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseT } } - static CompletableFuture> create(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType) { + public static CompletableFuture> create(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType) { MinecraftServer server = game.server(); GameResult result = game.lobby.manager.canStartGamePhase(definition); @@ -90,13 +95,35 @@ static CompletableFuture> create(GameInstance game, IGameP CompletableFuture> future = definition.getMap().open(server) .thenApplyAsync(r -> r.map(map -> { BehaviorList behaviors = definition.createBehaviors(); + if(game.definition.isMultiGamePhase()){ + return new MultiGamePhase(game, definition, phaseType, map, behaviors); + } return new GamePhase(game, definition, phaseType, map, behaviors); }), server); return GameResult.handleException("Unknown exception starting game phase", future); } + public static CompletableFuture> createMultiGame(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, ResourceLocation gameId) { + MinecraftServer server = game.server(); + + GameResult result = game.lobby.manager.canStartGamePhase(definition); + if (result.isError()) { + return CompletableFuture.completedFuture(result.castError()); + } - GameResult start() { + CompletableFuture> future = definition.getMap().open(server) + .thenApplyAsync(r -> r.map(map -> { + BehaviorList behaviors = definition.createBehaviors(); + if(game.definition.isMultiGamePhase()){ + return new MultiGamePhase(game, definition, phaseType, map, behaviors, gameId); + } + return new GamePhase(game, definition, phaseType, map, behaviors); + }), server); + + return GameResult.handleException("Unknown exception starting game phase", future); + } + + GameResult start(final boolean savePlayerDataToMemory) { try { behaviors.registerTo(this, events); } catch (GameException e) { @@ -117,7 +144,7 @@ GameResult start() { Collections.shuffle(shuffledPlayers); for (ServerPlayer player : shuffledPlayers) { - addAndSpawnPlayer(player, getRoleFor(player)); + addAndSpawnPlayer(player, getRoleFor(player), savePlayerDataToMemory); } invoker(GamePhaseEvents.START).start(); @@ -128,7 +155,7 @@ GameResult start() { return GameResult.ok(); } - private ServerPlayer addAndSpawnPlayer(ServerPlayer player, @Nullable PlayerRole role) { + protected ServerPlayer addAndSpawnPlayer(ServerPlayer player, @Nullable PlayerRole role, final boolean savePlayerDataToMemory) { SpawnBuilder spawn = new SpawnBuilder(player); invoker(GamePlayerEvents.SPAWN).onSpawn(player.getUUID(), spawn, role); @@ -140,12 +167,29 @@ private ServerPlayer addAndSpawnPlayer(ServerPlayer player, @Nullable PlayerRole addedPlayers.add(player.getUUID()); + if (savePlayerDataToMemory) { + game.playerStorage.setPlayerData(player, player.saveWithoutId(new CompoundTag())); + } + return newPlayer; } @Nullable GameStopReason tick() { try { + if (!pendingRunnables.isEmpty()) { + var itr = pendingRunnables.iterator(); + while (itr.hasNext()) { + var task = itr.next(); + if (task.counter <= 0) { + task.toRun.run(); + itr.remove(); + } else { + task.counter--; + } + } + } + invoker(GamePhaseEvents.TICK).tick(); } catch (Exception e) { cancelWithError(e); @@ -153,6 +197,11 @@ GameStopReason tick() { return stopped; } + @Override + public void schedule(float seconds, Runnable task) { + pendingRunnables.add(new ScheduledTask(task, (int)(server().tickRateManager().tickrate() * seconds))); + } + @Override public IGame game() { return game; @@ -213,7 +262,7 @@ private void onSetPlayerRole(ServerPlayer player, @Nullable PlayerRole role, @Nu void onPlayerJoin(ServerPlayer player) { try { - ServerPlayer newPlayer = addAndSpawnPlayer(player, null); + ServerPlayer newPlayer = addAndSpawnPlayer(player, null, false); invoker(GamePlayerEvents.JOIN).onAdd(newPlayer); } catch (Exception e) { LoveTropics.LOGGER.warn("Failed to dispatch player join event", e); @@ -315,4 +364,14 @@ public long ticks() { public boolean isActive() { return !destroyed; } + + private static class ScheduledTask { + private final Runnable toRun; + private int counter; + + private ScheduledTask(Runnable toRun, int counter) { + this.toRun = toRun; + this.counter = counter; + } + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java new file mode 100644 index 000000000..bb3fff579 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java @@ -0,0 +1,241 @@ +package com.lovetropics.minigames.common.core.game.impl; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.content.river_race.state.RiverRaceState; +import com.lovetropics.minigames.common.core.game.GamePhaseType; +import com.lovetropics.minigames.common.core.game.GameResult; +import com.lovetropics.minigames.common.core.game.GameStopReason; +import com.lovetropics.minigames.common.core.game.IGamePhaseDefinition; +import com.lovetropics.minigames.common.core.game.PlayerIsolation; +import com.lovetropics.minigames.common.core.game.behavior.BehaviorList; +import com.lovetropics.minigames.common.core.game.behavior.event.GameEventType; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.config.GameConfig; +import com.lovetropics.minigames.common.core.game.config.GameConfigs; +import com.lovetropics.minigames.common.core.game.map.GameMap; +import com.lovetropics.minigames.common.core.game.player.PlayerRole; +import com.lovetropics.minigames.common.core.game.rewards.GameRewardsMap; +import com.lovetropics.minigames.common.core.game.state.GameStateMap; +import com.lovetropics.minigames.common.core.game.state.IGameState; +import com.lovetropics.minigames.common.core.map.MapRegions; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Unit; +import net.minecraft.world.level.Level; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class MultiGamePhase extends GamePhase { + + @FunctionalInterface + interface GameStateRegistration { + void registerState(L lobby, M phase, R id); + } + + private enum MultiPhaseGameStates { + RIVER_RACE((lobby, phase, id) -> { + IGameState state = lobby.getMultiPhaseDataMap().get(id); + if (state instanceof RiverRaceState riverRaceState) { + riverRaceState.reset(); + } else { + lobby.getMultiPhaseDataMap().put(id, new RiverRaceState(phase)); + } + phase.phaseState.register(RiverRaceState.KEY, (RiverRaceState) lobby.getMultiPhaseDataMap().get(id)); + }); + + final GameStateRegistration registration; + + MultiPhaseGameStates(GameStateRegistration registration) { + this.registration = registration; + } + } + private ResourceLocation gameId; + @Nullable + private GamePhase activePhase = null; + @Nullable + private ResourceLocation activePhaseId = null; + private final List subPhaseGames = Lists.newArrayList(); + private static final Map gameStateMap = Maps.newHashMap(); + + protected MultiGamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors, ResourceLocation gameId) { + super(game, definition, phaseType, map, behaviors); + this.gameId = gameId; + gameStateMap.put(gameId, MultiPhaseGameStates.RIVER_RACE); + } + + protected MultiGamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { + this(game, definition, phaseType, map, behaviors, ResourceLocation.parse("lt:river_race")); + } + + public void registerState(final GameLobby lobby) { + final ResourceLocation id = game.definition.getId(); + gameStateMap.get(id).registration.registerState(lobby, this, id); + } + + public void setActivePhase(GamePhase activePhase, final boolean saveInventory, ResourceLocation activePhaseId) { + this.activePhase = activePhase; + this.activePhaseId = activePhaseId; + MultiGameManager.INSTANCE.addGamePhaseToDimension(activePhase.dimension(), activePhase); + activePhase.state().register(GameRewardsMap.STATE, ((GameLobby) lobby()).getRewardsMap()); + activePhase.state().register(RiverRaceState.KEY, (RiverRaceState) ((GameLobby) lobby()).createOrGetMultiPhaseState(this)); + activePhase.start(saveInventory); + } + + public ResourceLocation getActiveGameId(){ + if(activePhaseId != null){ + return activePhaseId; + } + return definition().getId(); + } + + public void returnHere(){ + List shuffledPlayers = Lists.newArrayList(allPlayers()); + Collections.shuffle(shuffledPlayers); + + for (ServerPlayer player : shuffledPlayers) { + returnPlayerToParentPhase(player, getRoleFor(player)); + } + } + + private void returnPlayerToParentPhase(ServerPlayer player, @Nullable PlayerRole role) { + // [Cojo] Added this event just in case we want to know when a player returns from a microgame, can remove if there's no usecase for it + invoker(GamePlayerEvents.RETURN).onReturn(player.getUUID(), role); + + ServerPlayer newPlayer = PlayerIsolation.INSTANCE.reloadPlayerFromMemory(game, player); + + invoker(GamePlayerEvents.ADD).onAdd(newPlayer); + invoker(GamePlayerEvents.SET_ROLE).onSetRole(newPlayer, role, null); + + addedPlayers.add(player.getUUID()); + } + + public List getSubPhaseGames() { + return subPhaseGames; + } + + public ResourceLocation getGameId() { + return gameId; + } + + @Override + public ResourceKey dimension() { + if(activePhase != null){ + return activePhase.dimension(); + } + return super.dimension(); + } + + @Override + public T invoker(GameEventType type) { + // Figure out what our sub-game phase is active and do that instead + if(activePhase != null){ + return activePhase.invoker(type); + } + return events.invoker(type); + } + + @Override + public GameInstance game(){ + return game; + } + + @Nullable + @Override + GameStopReason tick() { + // Also tick our current sub-game phase + if(activePhase != null){ + if(activePhase.tick() != null){ + MultiGameManager.INSTANCE.removeGamePhaseFromDimension(activePhase.dimension(), activePhase); + activePhase.destroy(); + activePhase = null; + activePhaseId = null; + + if (!startNextQueuedMicrogame(false)) { + returnHere(); + } + } + } else { + return super.tick(); + } + return null; + } + + @Override + public GameResult requestStop(GameStopReason reason) { + if(activePhase != null){ + if(reason.isFinished()){ + return activePhase.requestStop(GameStopReason.canceled()); + } + return activePhase.requestStop(reason); + } + return super.requestStop(reason); + } + + @Override + public MapRegions mapRegions() { + if(activePhase != null){ + return activePhase.mapRegions(); + } + return super.mapRegions(); + } + + + @Override + public ServerLevel level() { + if(activePhase != null){ + return activePhase.level(); + } + return super.level(); + } + + @Override + public GameStateMap state() { + if(activePhase != null){ + return activePhase.state(); + } + return super.state(); + } + + @Override + void destroy() { + if(activePhase != null){ + activePhase.destroy(); + activePhase = null; + activePhaseId = null; + return; + } + super.destroy(); + } + + public void clearQueuedGames() { + subPhaseGames.clear(); + } + + public void queueGames(List games) { + subPhaseGames.addAll(games); + } + + public boolean startNextQueuedMicrogame(final boolean saveInventory) { + // No queued games left + if (subPhaseGames.isEmpty()) { + return false; + } + + final ResourceLocation gameKey = subPhaseGames.removeFirst(); + GameConfig gameConfig = GameConfigs.REGISTRY.get(gameKey); + GamePhase.createMultiGame(game(), gameConfig.getPlayingPhase(), GamePhaseType.PLAYING, gameConfig.getId()).thenAccept((result) -> { + setActivePhase(result.getOk(), saveInventory, gameKey); + }); + + invoker(RiverRaceEvents.MICROGAME_STARTED).onMicrogameStarted(this); + + return true; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/player/PlayerStorage.java b/src/main/java/com/lovetropics/minigames/common/core/game/player/PlayerStorage.java new file mode 100644 index 000000000..c109b7b54 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/player/PlayerStorage.java @@ -0,0 +1,30 @@ +package com.lovetropics.minigames.common.core.game.player; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.UUID; + +/** + * In-memory storage of player data, useful when we want to store player data but not on disk + */ +public class PlayerStorage { + private final Object2ObjectMap storage = new Object2ObjectOpenHashMap<>(); + + public Optional fetchAndRemovePlayerData(final ServerPlayer player) { + @Nullable final CompoundTag compoundTag = storage.get(player.getUUID()); + if (compoundTag != null) { + storage.remove(player.getUUID()); + return Optional.of(compoundTag); + } + return Optional.empty(); + } + + public void setPlayerData(final ServerPlayer player, final CompoundTag tag) { + storage.put(player.getUUID(), tag); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/predicate/loot/IsMinigameCondition.java b/src/main/java/com/lovetropics/minigames/common/core/game/predicate/loot/IsMinigameCondition.java new file mode 100644 index 000000000..bd2a9b2b1 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/predicate/loot/IsMinigameCondition.java @@ -0,0 +1,42 @@ +package com.lovetropics.minigames.common.core.game.predicate.loot; + +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.impl.MultiGameManager; +import com.lovetropics.minigames.common.core.game.impl.MultiGamePhase; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.storage.loot.LootContext; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; +import net.neoforged.neoforge.common.loot.LootTableIdCondition; + +public class IsMinigameCondition implements LootItemCondition { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + builder -> builder + .group( + ResourceLocation.CODEC.fieldOf("minigame_id").forGetter(idCondition -> idCondition.minigameId)) + .apply(builder, IsMinigameCondition::new)); + private final ResourceLocation minigameId; + + private IsMinigameCondition(final ResourceLocation minigameId) { + this.minigameId = minigameId; + } + @Override + public LootItemConditionType getType() { + return LootItemConditions.IS_MINIGAME.value(); + } + + @Override + public boolean test(LootContext lootContext) { + IGamePhase gamePhaseInDimension = MultiGameManager.INSTANCE.getGamePhaseInDimension(lootContext.getLevel()); + if(gamePhaseInDimension == null){ + return false; + } + ResourceLocation gameId = gamePhaseInDimension.definition().getId(); + if(gamePhaseInDimension.definition().isMultiGamePhase()){ + gameId = ((MultiGamePhase)gamePhaseInDimension).getGameId(); + } + return gameId.equals(minigameId); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/predicate/loot/LootItemConditions.java b/src/main/java/com/lovetropics/minigames/common/core/game/predicate/loot/LootItemConditions.java new file mode 100644 index 000000000..3d59773d7 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/predicate/loot/LootItemConditions.java @@ -0,0 +1,22 @@ +package com.lovetropics.minigames.common.core.game.predicate.loot; + + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.qottott.Qottott; +import com.lovetropics.minigames.common.util.registry.LoveTropicsRegistrate; +import net.minecraft.core.Holder; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectCategory; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; + +public class LootItemConditions { + + private static final LoveTropicsRegistrate REGISTRATE = LoveTropics.registrate(); + public static final Holder IS_MINIGAME = REGISTRATE.object("is_minigame") + .lootItemConditionType(() -> new LootItemConditionType(IsMinigameCondition.CODEC)).register(); + + + public static void init() {} + +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java b/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java index d67bbc03d..74a8b19dc 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java @@ -10,6 +10,7 @@ import com.lovetropics.minigames.common.core.game.player.PlayerSet; import com.lovetropics.minigames.common.core.game.state.GameStateKey; import com.lovetropics.minigames.common.core.game.state.IGameState; +import com.lovetropics.minigames.common.core.game.state.statistics.PlayerKey; import com.lovetropics.minigames.common.core.game.util.TeamAllocator; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; @@ -101,6 +102,11 @@ public GameTeamKey getTeamForPlayer(Player player) { return getTeamForPlayer(player.getUUID()); } + @Nullable + public GameTeamKey getTeamForPlayer(PlayerKey player) { + return getTeamForPlayer(player.id()); + } + @Nullable public GameTeamKey getTeamForPlayer(UUID playerId) { for (Map.Entry entry : Object2ObjectMaps.fastIterable(playersByKey)) { diff --git a/src/main/java/com/lovetropics/minigames/common/core/network/LoveTropicsNetwork.java b/src/main/java/com/lovetropics/minigames/common/core/network/LoveTropicsNetwork.java index cd89e4378..33b8773c8 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/network/LoveTropicsNetwork.java +++ b/src/main/java/com/lovetropics/minigames/common/core/network/LoveTropicsNetwork.java @@ -11,6 +11,10 @@ import com.lovetropics.minigames.client.lobby.state.message.LobbyUpdateMessage; import com.lovetropics.minigames.client.particle_line.DrawParticleLineMessage; import com.lovetropics.minigames.client.toast.ShowNotificationToastMessage; +import com.lovetropics.minigames.common.core.network.trivia.RequestTriviaStateUpdateMessage; +import com.lovetropics.minigames.common.core.network.trivia.SelectTriviaAnswerMessage; +import com.lovetropics.minigames.common.core.network.trivia.ShowTriviaMessage; +import com.lovetropics.minigames.common.core.network.trivia.TriviaAnswerResponseMessage; import com.lovetropics.minigames.common.core.network.workspace.AddWorkspaceRegionMessage; import com.lovetropics.minigames.common.core.network.workspace.SetWorkspaceMessage; import com.lovetropics.minigames.common.core.network.workspace.UpdateWorkspaceRegionMessage; @@ -51,5 +55,10 @@ public static void register(RegisterPayloadHandlersEvent event) { registrar.playToClient(SpectatorPlayerActivityMessage.TYPE, SpectatorPlayerActivityMessage.STREAM_CODEC, SpectatorPlayerActivityMessage::handle); registrar.playToClient(RiseTideMessage.TYPE, RiseTideMessage.STREAM_CODEC, RiseTideMessage::handle); + + registrar.playToClient(ShowTriviaMessage.TYPE, ShowTriviaMessage.STREAM_CODEC, ShowTriviaMessage::handle); + registrar.playToServer(SelectTriviaAnswerMessage.TYPE, SelectTriviaAnswerMessage.STREAM_CODEC, SelectTriviaAnswerMessage::handle); + registrar.playToServer(RequestTriviaStateUpdateMessage.TYPE, RequestTriviaStateUpdateMessage.STREAM_CODEC, RequestTriviaStateUpdateMessage::handle); + registrar.playToClient(TriviaAnswerResponseMessage.TYPE, TriviaAnswerResponseMessage.STREAM_CODEC, TriviaAnswerResponseMessage::handle); } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/network/trivia/RequestTriviaStateUpdateMessage.java b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/RequestTriviaStateUpdateMessage.java new file mode 100644 index 000000000..ceefd360b --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/RequestTriviaStateUpdateMessage.java @@ -0,0 +1,38 @@ +package com.lovetropics.minigames.common.core.network.trivia; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.river_race.TriviaEvents; +import com.lovetropics.minigames.common.content.river_race.block.HasTrivia; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import com.lovetropics.minigames.common.core.game.IGameManager; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public record RequestTriviaStateUpdateMessage(BlockPos triviaBlock) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(LoveTropics.location("request_trivia_update")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, RequestTriviaStateUpdateMessage::triviaBlock, + RequestTriviaStateUpdateMessage::new + ); + + public static void handle(final RequestTriviaStateUpdateMessage message, final IPayloadContext context) { + if(context.player().level().getBlockEntity(message.triviaBlock()) instanceof HasTrivia triviaBlockEntity){ + IGamePhase game = IGameManager.get().getGamePhaseFor(context.player()); + if (game != null) { + ServerPlayer player = (ServerPlayer) context.player(); + PacketDistributor.sendToPlayer(player, new TriviaAnswerResponseMessage(message.triviaBlock(), triviaBlockEntity.getState())); + } + } + } + @Override + public Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/network/trivia/SelectTriviaAnswerMessage.java b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/SelectTriviaAnswerMessage.java new file mode 100644 index 000000000..f5cc9f8a1 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/SelectTriviaAnswerMessage.java @@ -0,0 +1,43 @@ +package com.lovetropics.minigames.common.core.network.trivia; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.river_race.TriviaEvents; +import com.lovetropics.minigames.common.content.river_race.block.HasTrivia; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import com.lovetropics.minigames.common.core.game.IGameManager; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public record SelectTriviaAnswerMessage(BlockPos triviaBlock, String selectedAnswer) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(LoveTropics.location("select_trivia_answer")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, SelectTriviaAnswerMessage::triviaBlock, + ByteBufCodecs.STRING_UTF8, SelectTriviaAnswerMessage::selectedAnswer, + SelectTriviaAnswerMessage::new + ); + + public static void handle(final SelectTriviaAnswerMessage message, final IPayloadContext context) { + if (context.player().level().getBlockEntity(message.triviaBlock()) instanceof final HasTrivia triviaBlockEntity){ + IGamePhase game = IGameManager.get().getGamePhaseFor(context.player()); + if (game != null) { + ServerPlayer player = (ServerPlayer) context.player(); + boolean isCorrect = game.invoker(TriviaEvents.ANSWER_TRIVIA_BLOCK_QUESTION) + .onAnswerQuestion(player, player.serverLevel(), message.triviaBlock(), + triviaBlockEntity, + triviaBlockEntity.getQuestion(), message.selectedAnswer()); + + } +// triviaBlockEntity.handleAnswerSelection(context.player(), message.selectedAnswer()); + } + } + @Override + public Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/network/trivia/ShowTriviaMessage.java b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/ShowTriviaMessage.java new file mode 100644 index 000000000..5894d82a7 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/ShowTriviaMessage.java @@ -0,0 +1,34 @@ +package com.lovetropics.minigames.common.core.network.trivia; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.client.game.trivia.ClientTriviaHandler; +import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.util.Optional; + +public record ShowTriviaMessage(BlockPos triviaBlock, TriviaBehaviour.TriviaQuestion question, TriviaBlockEntity.TriviaBlockState triviaBlockState) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>(LoveTropics.location("show_trivia")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, ShowTriviaMessage::triviaBlock, + TriviaBehaviour.TriviaQuestion.STREAM_CODEC, ShowTriviaMessage::question, + TriviaBlockEntity.TriviaBlockState.STREAM_CODEC, ShowTriviaMessage::triviaBlockState, + ShowTriviaMessage::new + ); + + public static void handle(final ShowTriviaMessage message, final IPayloadContext context) { + ClientTriviaHandler.showScreen(message); + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/network/trivia/TriviaAnswerResponseMessage.java b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/TriviaAnswerResponseMessage.java new file mode 100644 index 000000000..4821c2c80 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/network/trivia/TriviaAnswerResponseMessage.java @@ -0,0 +1,29 @@ +package com.lovetropics.minigames.common.core.network.trivia; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.client.game.trivia.ClientTriviaHandler; +import com.lovetropics.minigames.common.content.river_race.block.TriviaBlockEntity; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public record TriviaAnswerResponseMessage(BlockPos triviaBlock, TriviaBlockEntity.TriviaBlockState triviaBlockState) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>(LoveTropics.location("trivia_answer_response")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, TriviaAnswerResponseMessage::triviaBlock, + TriviaBlockEntity.TriviaBlockState.STREAM_CODEC, TriviaAnswerResponseMessage::triviaBlockState, + TriviaAnswerResponseMessage::new + ); + public static void handle(final TriviaAnswerResponseMessage message, final IPayloadContext context) { + ClientTriviaHandler.handleResponse(message); + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/util/SequentialList.java b/src/main/java/com/lovetropics/minigames/common/util/SequentialList.java new file mode 100644 index 000000000..5fa51b7bf --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/util/SequentialList.java @@ -0,0 +1,29 @@ +package com.lovetropics.minigames.common.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class SequentialList { + private final List list; + private int index; + + public SequentialList(List list, int index) { + this.list = list; + this.index = index; + } + + public T current() { + return list.get(index); + } + + public T next() { + if (index >= list.size() - 1) { + index = 0; + } else { + index++; + } + + return current(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/util/registry/LootItemConditionTypeBuilder.java b/src/main/java/com/lovetropics/minigames/common/util/registry/LootItemConditionTypeBuilder.java new file mode 100644 index 000000000..3e86585c4 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/util/registry/LootItemConditionTypeBuilder.java @@ -0,0 +1,35 @@ +package com.lovetropics.minigames.common.util.registry; + +import com.lovetropics.minigames.common.core.game.predicate.entity.EntityPredicate; +import com.lovetropics.minigames.common.core.game.predicate.entity.EntityPredicates; +import com.lovetropics.minigames.common.core.game.predicate.loot.LootItemConditions; +import com.mojang.serialization.MapCodec; +import com.tterrag.registrate.builders.AbstractBuilder; +import com.tterrag.registrate.builders.BuilderCallback; +import com.tterrag.registrate.util.entry.RegistryEntry; +import com.tterrag.registrate.util.nullness.NonnullType; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; +import net.neoforged.neoforge.registries.DeferredHolder; + +import java.util.function.Supplier; + +public final class LootItemConditionTypeBuilder extends AbstractBuilder> { + private final Supplier condition; + + public LootItemConditionTypeBuilder(LoveTropicsRegistrate owner, P parent, String name, BuilderCallback callback, Supplier condition) { + super(owner, parent, name, callback, Registries.LOOT_CONDITION_TYPE); + this.condition = condition; + } + + @Override + protected @NonnullType T createEntry() { + return condition.get(); + } + @Override + protected RegistryEntry createEntryWrapper(final DeferredHolder delegate) { + return new RegistryEntry<>(getOwner(), delegate); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/util/registry/LoveTropicsRegistrate.java b/src/main/java/com/lovetropics/minigames/common/util/registry/LoveTropicsRegistrate.java index 91ce4e48a..29484b096 100644 --- a/src/main/java/com/lovetropics/minigames/common/util/registry/LoveTropicsRegistrate.java +++ b/src/main/java/com/lovetropics/minigames/common/util/registry/LoveTropicsRegistrate.java @@ -5,8 +5,12 @@ import com.lovetropics.minigames.common.core.game.predicate.entity.EntityPredicate; import com.mojang.serialization.MapCodec; import com.tterrag.registrate.AbstractRegistrate; +import com.tterrag.registrate.util.entry.RegistryEntry; +import net.minecraft.core.registries.Registries; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; import net.neoforged.fml.ModLoadingContext; import java.util.function.Function; @@ -45,6 +49,18 @@ public EntityPredicateBuilder entityPredica return entry(name, callback -> new EntityPredicateBuilder<>(this, parent, name, callback, codec)); } + public LootItemConditionTypeBuilder lootItemConditionType(Supplier lootItemConditionTypeSupplier) { + return lootItemConditionType(this, lootItemConditionTypeSupplier); + } + + public LootItemConditionTypeBuilder lootItemConditionType(P parent, Supplier lootItemConditionTypeSupplier) { + return lootItemConditionType(parent, currentName(), lootItemConditionTypeSupplier); + } + + public LootItemConditionTypeBuilder lootItemConditionType(P parent, String name, Supplier lootItemConditionTypeSupplier) { + return entry(name, callback -> new LootItemConditionTypeBuilder<>(this, parent, name, callback, lootItemConditionTypeSupplier)); + } + public GameClientTweakBuilder clientState(MapCodec codec) { return clientState(this, codec); } diff --git a/src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java b/src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java new file mode 100644 index 000000000..dc91adf83 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java @@ -0,0 +1,33 @@ +package com.lovetropics.minigames.mixin; + +import com.lovetropics.minigames.common.core.game.IGameManager; +import com.lovetropics.minigames.common.core.game.behavior.event.GameWorldEvents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FallingBlockEntity.class) +public abstract class FallingBlockEntityMixin extends Entity { + + public FallingBlockEntityMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + @Shadow + private BlockState blockState; + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/item/FallingBlockEntity;discard()V", ordinal = 1, shift = At.Shift.AFTER), method = "tick") + private void customFalling(CallbackInfo ci) { + var game = IGameManager.get().getGamePhaseAt(level(), blockPosition()); + if (game != null) { + game.invoker(GameWorldEvents.BLOCK_LANDED).onBlockLanded(level(), blockPosition(), blockState); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java new file mode 100644 index 000000000..68942e8fc --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java @@ -0,0 +1,50 @@ +package com.lovetropics.minigames.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ImageButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.RecipeBookType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin({CraftingScreen.class, InventoryScreen.class}) +public class HiddenRecipeBookMixin { + @Final + @Shadow(remap = false) + private RecipeBookComponent recipeBookComponent; + + @Inject(method = "init", at = @At("HEAD")) + private void hideBookIfOpen(CallbackInfo ci) { + if (ClientGameStateManager.getOrNull(GameClientStateTypes.HIDE_RECIPE_BOOK) != null) { + Minecraft.getInstance().player.getRecipeBook().setBookSetting(RecipeBookType.CRAFTING, false, false); + } + } + + @WrapOperation(method = "init", at = @At(value = "NEW", target = "net/minecraft/client/gui/components/ImageButton")) + private ImageButton respectHiddenBook(int x, int y, int width, int height, WidgetSprites sprites, Button.OnPress onPress, Operation original) { + var disabled = ResourceLocation.fromNamespaceAndPath("ltminigames", "recipe_book/button_disabled"); + var org = original.call(x, y, width, height, new WidgetSprites( + sprites.enabled(), disabled, sprites.enabledFocused(), disabled + ), onPress); + var hidden = ClientGameStateManager.getOrNull(GameClientStateTypes.HIDE_RECIPE_BOOK); + if (hidden != null) { + org.active = false; + org.setTooltip(Tooltip.create(hidden.message())); + } + return org; + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java new file mode 100644 index 000000000..2bba29d54 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java @@ -0,0 +1,22 @@ +package com.lovetropics.minigames.mixin.client; + +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.player.Input; +import net.minecraft.client.player.KeyboardInput; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(KeyboardInput.class) +public class KeyboardInputMixin extends Input { + @Inject(method = "tick", at = @At("TAIL")) + private void respectSwappedMovement(boolean isSneaking, float sneakingSpeedMultiplier, CallbackInfo ci) { + if (ClientGameStateManager.getOrNull(GameClientStateTypes.SWAP_MOVEMENT) != null) { + float oldLeft = leftImpulse; + leftImpulse = forwardImpulse; + forwardImpulse = oldLeft; + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java new file mode 100644 index 000000000..81eb7d8f3 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java @@ -0,0 +1,37 @@ +package com.lovetropics.minigames.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.Minecraft; +import net.minecraft.client.MouseHandler; +import net.minecraft.client.player.LocalPlayer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MouseHandler.class) +public class MouseHandlerMixin { + @Shadow + @Final + private Minecraft minecraft; + + @WrapOperation(method = "turnPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;turn(DD)V")) + private void respectControlModificationState(LocalPlayer player, double dx, double dy, Operation original) { + var state = ClientGameStateManager.getOrNull(GameClientStateTypes.INVERT_CONTROLS); + if (state != null) { + if (state.xAxis()) { + dx = dx * -1; + } + + // Avoid allowing the bypass of the setting by modifying the options + if (state.yAxis() && !minecraft.options.invertYMouse().get()) { + dy = dy * -1; + } + } + + original.call(player, dx, dy); + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 32bdb918b..a8c47f49e 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -19,3 +19,8 @@ public net.minecraft.world.entity.monster.Creeper explodeCreeper()V # Collisions public net.minecraft.world.entity.Entity collideWithShapes(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/List;)Lnet/minecraft/world/phys/Vec3; + +public net.minecraft.client.renderer.RenderType$CompositeRenderType +public net.minecraft.client.renderer.RenderType$CompositeRenderType state()Lnet/minecraft/client/renderer/RenderType$CompositeState; +public net.minecraft.client.renderer.RenderType$CompositeState textureState +public net.minecraft.client.renderer.RenderStateShard$TextureStateShard texture diff --git a/src/main/resources/assets/ltminigames/textures/block/trivia_collectable.png b/src/main/resources/assets/ltminigames/textures/block/trivia_collectable.png new file mode 100644 index 000000000..5da0ca99f Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/block/trivia_collectable.png differ diff --git a/src/main/resources/assets/ltminigames/textures/block/trivia_gate.png b/src/main/resources/assets/ltminigames/textures/block/trivia_gate.png new file mode 100644 index 000000000..af368df41 Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/block/trivia_gate.png differ diff --git a/src/main/resources/assets/ltminigames/textures/block/trivia_reward.png b/src/main/resources/assets/ltminigames/textures/block/trivia_reward.png new file mode 100644 index 000000000..3add61eef Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/block/trivia_reward.png differ diff --git a/src/main/resources/assets/ltminigames/textures/entity/chest/trivia.png b/src/main/resources/assets/ltminigames/textures/entity/chest/trivia.png new file mode 100644 index 000000000..08e6407f1 Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/entity/chest/trivia.png differ diff --git a/src/main/resources/assets/ltminigames/textures/entity/chest/trivia_left.png b/src/main/resources/assets/ltminigames/textures/entity/chest/trivia_left.png new file mode 100644 index 000000000..049b7f97b Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/entity/chest/trivia_left.png differ diff --git a/src/main/resources/assets/ltminigames/textures/entity/chest/trivia_right.png b/src/main/resources/assets/ltminigames/textures/entity/chest/trivia_right.png new file mode 100644 index 000000000..0462dd9fe Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/entity/chest/trivia_right.png differ diff --git a/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/crafting_grid.png b/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/crafting_grid.png new file mode 100644 index 000000000..6bcd16977 Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/crafting_grid.png differ diff --git a/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/items_bar.png b/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/items_bar.png new file mode 100644 index 000000000..d353aad4c Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/items_bar.png differ diff --git a/src/main/resources/assets/ltminigames/textures/gui/sprites/recipe_book/button_disabled.png b/src/main/resources/assets/ltminigames/textures/gui/sprites/recipe_book/button_disabled.png new file mode 100644 index 000000000..3d22601ba Binary files /dev/null and b/src/main/resources/assets/ltminigames/textures/gui/sprites/recipe_book/button_disabled.png differ diff --git a/src/main/resources/ltminigames.mixins.json b/src/main/resources/ltminigames.mixins.json index b9a20aebb..001bd71da 100644 --- a/src/main/resources/ltminigames.mixins.json +++ b/src/main/resources/ltminigames.mixins.json @@ -13,6 +13,7 @@ "SimpleRegistryMixin", "TagManagerAccessor", "WorldGenSettingsMixin", + "FallingBlockEntityMixin", "chat.ServerGamePacketListenerImplMixin", "disguise.LivingEntityMixin", "gametest.GameTestHelperAccess", @@ -28,7 +29,10 @@ "client.IntegratedPlayerListMixin", "client.MinecraftMixin", "client.PlayerTabOverlayGuiMixin", - "client.PoseStackAccessor" + "client.PoseStackAccessor", + "client.HiddenRecipeBookMixin", + "client.MouseHandlerMixin", + "client.KeyboardInputMixin" ], "injectors": { "defaultRequire": 1