commit 6052e00429a31bd000d9d86b9ddaef05752f7434 Author: TheBrokenRail Date: Sun Mar 1 13:19:59 2020 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9563893 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# fabric + +run/ \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..6ef8c14 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,19 @@ +pipeline { + agent { + docker { + image 'openjdk:8-jdk' + } + } + stages { + stage('Build') { + steps { + sh './gradlew jar' + } + post { + success { + archiveArtifacts artifacts: 'build/libs/*', fingerprint: true + } + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..886b254 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 TheBrokenRail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa6a954 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# SorceryCraft +Cast Spells in Minecraft! + +This mod supports the Minecraft 1.16 snapshots. + +## Spells +Spells are found throughout the world in chests. When you pick up a Spell you will learn it, one you learn a Spell you can apply it to a blank or existing Spell in the Casting Table. You can cast Spells by right-clicking. + +## Casting Table +You can apply Spells to blank or existing Spells in the Casting Table. Doing so will require a certain amount of levels, and sometimes may require an item. + +## Crafting +#### Blank Spell +Craft a blank Spell by placing a Paper in the center of a Crafting Table, and surround it with 4 Lapis Lazuli in a diamond shape. + +#### Casting table +Craft a Casting Table by placing a Diamond in the center of a Crafting Table, place 4 Lapis Lazuli in the corners, and fill in the rest with 4 Cobblestone. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..8c08066 --- /dev/null +++ b/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'fabric-loom' version '0.2.6-SNAPSHOT' + id 'maven-publish' +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +archivesBaseName = project.archives_base_name +version = project.mod_version +group = project.maven_group + +minecraft { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modCompile "net.fabricmc:fabric-loader:${project.loader_version}" + + modCompile "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" +} + +processResources { + inputs.property "version", project.version + + from(sourceSets.main.resources.srcDirs) { + include "fabric.mod.json" + expand "version": project.version + } + + from(sourceSets.main.resources.srcDirs) { + exclude "fabric.mod.json" + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier "sources" + from sourceSets.main.allSource +} + +jar { + from "LICENSE" +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + // add all the jars that should be included when publishing to maven + artifact(remapJar) { + builtBy remapJar + } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } + + // select the repositories you want to publish to + repositories { + // uncomment to publish to the local maven + // mavenLocal() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2245063 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs = -Xmx1G + +# Fabric Properties + # check these on https://fabricmc.net/use + minecraft_version = 20w09a + yarn_mappings = 20w09a+build.6 + loader_version = 0.7.8+build.184 + +# Mod Properties + mod_version = 1.0.0 + maven_group = com.thebrokenrail + archives_base_name = sorcerycraft + +# Dependencies + # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api + fabric_version = 0.4.33+build.301-1.16 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6660f71 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Feb 29 21:58:32 EST 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..8e25e6c --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5b60df3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/SorceryCraft.java b/src/main/java/com/thebrokenrail/sorcerycraft/SorceryCraft.java new file mode 100644 index 0000000..2be39f8 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/SorceryCraft.java @@ -0,0 +1,122 @@ +package com.thebrokenrail.sorcerycraft; + +import com.thebrokenrail.sorcerycraft.block.CastingTableBlock; +import com.thebrokenrail.sorcerycraft.block.CastingTableContainer; +import com.thebrokenrail.sorcerycraft.client.block.CastingTableScreen; +import com.thebrokenrail.sorcerycraft.client.entity.SpellEntityRenderer; +import com.thebrokenrail.sorcerycraft.command.SpellCommand; +import com.thebrokenrail.sorcerycraft.entity.SpellEntity; +import com.thebrokenrail.sorcerycraft.item.SpellItem; +import com.thebrokenrail.sorcerycraft.packet.LearnedNewSpellS2CPacket; +import com.thebrokenrail.sorcerycraft.packet.SelectSpellC2SPacket; +import com.thebrokenrail.sorcerycraft.packet.UpdateKnownSpellsS2CPacket; +import com.thebrokenrail.sorcerycraft.spell.RandomSpellLootTableFunction; +import com.thebrokenrail.sorcerycraft.spell.SpellRegistry; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.client.itemgroup.FabricItemGroupBuilder; +import net.fabricmc.fabric.api.client.rendereregistry.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.screen.ScreenProviderRegistry; +import net.fabricmc.fabric.api.container.ContainerProviderRegistry; +import net.fabricmc.fabric.api.entity.FabricEntityTypeBuilder; +import net.fabricmc.fabric.api.loot.v1.FabricLootPoolBuilder; +import net.fabricmc.fabric.api.loot.v1.event.LootTableLoadingCallback; +import net.fabricmc.fabric.api.registry.CommandRegistry; +import net.fabricmc.fabric.impl.networking.ClientSidePacketRegistryImpl; +import net.fabricmc.fabric.impl.networking.ServerSidePacketRegistryImpl; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.EntityCategory; +import net.minecraft.entity.EntityDimensions; +import net.minecraft.entity.EntityType; +import net.minecraft.item.*; +import net.minecraft.loot.BinomialLootTableRange; +import net.minecraft.loot.LootTables; +import net.minecraft.loot.entry.ItemEntry; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.World; + +import java.util.Objects; + +public class SorceryCraft implements ModInitializer, ClientModInitializer { + public static final String NAMESPACE = "sorcerycraft"; + public static SpellItem SPELL_ITEM; + public static CastingTableBlock CASTING_TABLE_BLOCK; + public static BlockItem CASTING_TABLE_BLOCK_ITEM; + public static ItemGroup ITEM_GROUP; + public static EntityType SPELL_ENTITY; + public static final Identifier[] LOOT_TABLES = new Identifier[] { + LootTables.SIMPLE_DUNGEON_CHEST, + LootTables.END_CITY_TREASURE_CHEST, + LootTables.NETHER_BRIDGE_CHEST, + LootTables.ABANDONED_MINESHAFT_CHEST, + LootTables.SHIPWRECK_TREASURE_CHEST, + LootTables.DESERT_PYRAMID_CHEST, + LootTables.JUNGLE_TEMPLE_CHEST, + LootTables.STRONGHOLD_LIBRARY_CHEST + }; + + private boolean isSelectedLootTable(Identifier lootTable) { + for (Identifier id : LOOT_TABLES) { + if (id.equals(lootTable)) { + return true; + } + } + return false; + } + + @Override + public void onInitialize() { + ITEM_GROUP = FabricItemGroupBuilder.create( + new Identifier(NAMESPACE, "spells")) + .icon(() -> new ItemStack(SPELL_ITEM)) + .build(); + + CASTING_TABLE_BLOCK = new CastingTableBlock(); + CASTING_TABLE_BLOCK_ITEM = new BlockItem(CASTING_TABLE_BLOCK, new Item.Settings().group(ITEM_GROUP)); + SPELL_ITEM = new SpellItem(); + + SPELL_ENTITY = FabricEntityTypeBuilder.create(EntityCategory.MISC, SpellEntity::new).size(EntityDimensions.fixed(0.25f, 0.25f)).build(); + + Registry.register(Registry.ITEM, new Identifier(NAMESPACE, "spell"), SPELL_ITEM); + Registry.register(Registry.BLOCK, new Identifier(NAMESPACE, "casting_table"), CASTING_TABLE_BLOCK); + Registry.register(Registry.ITEM, new Identifier(NAMESPACE, "casting_table"), CASTING_TABLE_BLOCK_ITEM); + Registry.register(Registry.ENTITY_TYPE, new Identifier(NAMESPACE, "spell"), SPELL_ENTITY); + + SpellRegistry.init(); + + ContainerProviderRegistry.INSTANCE.registerFactory(new Identifier(NAMESPACE, "casting_table"), (syncId, identifier, player, buf) -> { + final World world = player.world; + final BlockPos pos = buf.readBlockPos(); + return Objects.requireNonNull(world.getBlockState(pos).createContainerFactory(player.world, pos)).createMenu(syncId, player.inventory, player); + }); + + CommandRegistry.INSTANCE.register(false, SpellCommand::register); + + ServerSidePacketRegistryImpl.INSTANCE.register(new Identifier(NAMESPACE, "select_spell"), SelectSpellC2SPacket::handle); + ClientSidePacketRegistryImpl.INSTANCE.register(new Identifier(NAMESPACE, "update_known_spells"), UpdateKnownSpellsS2CPacket::handle); + ClientSidePacketRegistryImpl.INSTANCE.register(new Identifier(NAMESPACE, "learned_new_spell"), LearnedNewSpellS2CPacket::handle); + + LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> { + if (isSelectedLootTable(id)) { + FabricLootPoolBuilder poolBuilder = FabricLootPoolBuilder.builder() + .withRolls(new BinomialLootTableRange(2, 0.5f)) + .withEntry(ItemEntry.builder(SPELL_ITEM)) + .withFunction(new RandomSpellLootTableFunction.Builder()); + + supplier.withPool(poolBuilder); + } + }); + } + + @Override + public void onInitializeClient() { + EntityRendererRegistry.INSTANCE.register(SPELL_ENTITY, (entityRenderDispatcher, context) -> new SpellEntityRenderer(entityRenderDispatcher)); + ScreenProviderRegistry.INSTANCE.registerFactory(new Identifier(NAMESPACE, "casting_table"), (container) -> { + assert MinecraftClient.getInstance().player != null; + return new CastingTableScreen(container, MinecraftClient.getInstance().player.inventory, new TranslatableText("block.sorcerycraft.casting_table")); + }); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/block/CastingTableBlock.java b/src/main/java/com/thebrokenrail/sorcerycraft/block/CastingTableBlock.java new file mode 100644 index 0000000..4ce8d69 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/block/CastingTableBlock.java @@ -0,0 +1,39 @@ +package com.thebrokenrail.sorcerycraft.block; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import net.fabricmc.fabric.api.block.FabricBlockSettings; +import net.fabricmc.fabric.api.container.ContainerProviderRegistry; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Material; +import net.minecraft.container.BlockContext; +import net.minecraft.container.NameableContainerFactory; +import net.minecraft.container.SimpleNamedContainerFactory; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +@SuppressWarnings("deprecation") +public class CastingTableBlock extends Block { + public CastingTableBlock() { + super(FabricBlockSettings.of(Material.STONE).strength(2.0F, 6.0F).build()); + } + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + if (!world.isClient()) { + ContainerProviderRegistry.INSTANCE.openContainer(new Identifier(SorceryCraft.NAMESPACE, "casting_table"), player, (buf) -> buf.writeBlockPos(pos)); + } + return ActionResult.SUCCESS; + } + + @Override + public NameableContainerFactory createContainerFactory(BlockState state, World world, BlockPos pos) { + return new SimpleNamedContainerFactory((i, playerInventory, playerEntity) -> new CastingTableContainer(i, playerInventory, BlockContext.create(world, pos)), new TranslatableText("container.sorcerycraft.casting_table")); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/block/CastingTableContainer.java b/src/main/java/com/thebrokenrail/sorcerycraft/block/CastingTableContainer.java new file mode 100644 index 0000000..0218f45 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/block/CastingTableContainer.java @@ -0,0 +1,190 @@ +package com.thebrokenrail.sorcerycraft.block; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import com.thebrokenrail.sorcerycraft.spell.Spell; +import com.thebrokenrail.sorcerycraft.spell.SpellPlayerEntity; +import com.thebrokenrail.sorcerycraft.spell.SpellRegistry; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import net.minecraft.container.*; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.BasicInventory; +import net.minecraft.inventory.CraftingResultInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.sound.SoundEvents; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CastingTableContainer extends Container { + private final Inventory inventory; + private final Inventory result; + private final Spell[] spells; + private final BlockContext context; + private int index = 0; + + public CastingTableContainer(int syncId, PlayerInventory playerInventory, BlockContext blockContext) { + super(ContainerType.STONECUTTER, syncId); + + inventory = new BasicInventory(2) { + public void markDirty() { + super.markDirty(); + CastingTableContainer.this.onContentChanged(this); + } + }; + context = blockContext; + result = new CraftingResultInventory(); + + if (playerInventory.player.isCreative()) { + spells = SpellRegistry.getSpells(); + } else { + SpellPlayerEntity spellPlayer = (SpellPlayerEntity) playerInventory.player; + Map spellsMap = spellPlayer.getSpells(); + List spellsArray = new ArrayList<>(); + + Spell[] allSpells = SpellRegistry.getSpells(); + for (Spell spell : allSpells) { + if (spellsMap.containsKey(spell.getID()) && spellsMap.get(spell.getID()) >= spell.getLevel()) { + spellsArray.add(spell); + } + } + spells = spellsArray.toArray(new Spell[0]); + } + + addSlot(new Slot(inventory, 0, 136, 37) { + @Override + public boolean canInsert(ItemStack stack) { + return stack.getItem().equals(SorceryCraft.SPELL_ITEM); + } + + @Override + public int getMaxStackAmount() { + return 1; + } + + @Override + public void onStackChanged(ItemStack originalItem, ItemStack itemStack) { + super.onStackChanged(originalItem, itemStack); + markDirty(); + } + }); + addSlot(new Slot(inventory, 1, 162, 37) { + @Override + public void onStackChanged(ItemStack originalItem, ItemStack itemStack) { + super.onStackChanged(originalItem, itemStack); + markDirty(); + } + }); + addSlot(new Slot(result, 2, 220, 37) { + public boolean canInsert(ItemStack stack) { + return false; + } + + public boolean canTakeItems(PlayerEntity playerEntity) { + return canTakeResult(playerEntity) && hasStack(); + } + + public ItemStack onTakeItem(PlayerEntity player, ItemStack stack) { + if (!player.isCreative()) { + player.addExperienceLevels(-spells[index].getXPCost()); + } + + player.playSound(SoundEvents.BLOCK_ENCHANTMENT_TABLE_USE, 1.0f, 1.0f); + + CastingTableContainer.this.inventory.setInvStack(0, ItemStack.EMPTY); + CastingTableContainer.this.inventory.takeInvStack(1, spells[index].getItemCost().getCount()); + return stack; + } + }); + + int k; + for (k = 0; k < 3; ++k) { + for(int j = 0; j < 9; ++j) { + addSlot(new Slot(playerInventory, j + k * 9 + 9, 108 + j * 18, 84 + k * 18)); + } + } + + for (k = 0; k < 9; ++k) { + addSlot(new Slot(playerInventory, k, 108 + k * 18, 142)); + } + } + + public boolean canTakeResult(PlayerEntity playerEntity) { + return (playerEntity.isCreative() || playerEntity.experienceLevel >= spells[index].getXPCost()) && spells[index].getXPCost() > 0; + } + + public void setIndex(int index) { + this.index = index; + onContentChanged(inventory); + + if (inventory.getInvStack(0).isEmpty() && inventory.getInvStack(1).isEmpty()) { + ItemStack spellItem = new ItemStack(SorceryCraft.SPELL_ITEM); + autoFill(0, spellItem, true); + ItemStack paymentItem = getRecipes()[index].getItemCost(); + autoFill(1, paymentItem, false); + } + } + + @Override + public void close(PlayerEntity player) { + super.close(player); + context.run((world, blockPos) -> { + dropInventory(player, world, inventory); + }); + } + + @Override + public void onContentChanged(Inventory inventory) { + super.onContentChanged(inventory); + ItemStack item = inventory.getInvStack(0); + ItemStack cost = inventory.getInvStack(1); + if (inventory == this.inventory) { + if (spells.length > 0 && + !item.isEmpty() && + cost.getItem() == spells[index].getItemCost().getItem() && + cost.getCount() >= spells[index].getItemCost().getCount()) { + ItemStack resultItem = item.copy(); + Map resultSpells = SpellTag.getSpells(resultItem); + if (!resultSpells.containsKey(spells[index].getID()) || resultSpells.get(spells[index].getID()) <= spells[index].getLevel()) { + resultSpells.put(spells[index].getID(), spells[index].getLevel()); + } + SpellTag.setSpells(resultItem, resultSpells); + result.setInvStack(2, resultItem); + } else { + result.setInvStack(2, ItemStack.EMPTY); + } + } + } + + private void autoFill(int slot, ItemStack stack, boolean onlyOne) { + if (!stack.isEmpty()) { + for (int i = 3; i < 39; ++i) { + ItemStack itemStack = slots.get(i).getStack(); + if (!itemStack.isEmpty() && stack.getItem() == itemStack.getItem()) { + ItemStack invSlot = inventory.getInvStack(slot); + int count = invSlot.isEmpty() ? 0 : invSlot.getCount(); + int requiredCount = Math.min((onlyOne ? 1 : stack.getMaxCount()) - count, itemStack.getCount()); + ItemStack modifiedItem = itemStack.copy(); + int totalCount = count + requiredCount; + itemStack.decrement(requiredCount); + modifiedItem.setCount(totalCount); + inventory.setInvStack(slot, modifiedItem); + if (totalCount >= stack.getMaxCount() || onlyOne) { + break; + } + } + } + } + } + + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + + public Spell[] getRecipes() { + return spells; + } +} \ No newline at end of file diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/client/block/CastingTableScreen.java b/src/main/java/com/thebrokenrail/sorcerycraft/client/block/CastingTableScreen.java new file mode 100644 index 0000000..189dc68 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/client/block/CastingTableScreen.java @@ -0,0 +1,235 @@ +package com.thebrokenrail.sorcerycraft.client.block; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.thebrokenrail.sorcerycraft.block.CastingTableContainer; +import com.thebrokenrail.sorcerycraft.packet.SelectSpellC2SPacket; +import com.thebrokenrail.sorcerycraft.spell.Spell; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.screen.ingame.ContainerScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +@Environment(EnvType.CLIENT) +public class CastingTableScreen extends ContainerScreen { + private static final Identifier TEXTURE = new Identifier("textures/gui/container/villager2.png"); + private int selectedIndex; + private int indexStartOffset; + private boolean scrolling; + + public CastingTableScreen(CastingTableContainer container, PlayerInventory inventory, Text title) { + super(container, inventory, title); + containerWidth = 276; + } + + @Override + protected void drawForeground(int mouseX, int mouseY) { + int j = containerHeight - 94; + font.draw(title.asFormattedString(), (float) (49 + this.containerWidth / 2 - font.getStringWidth(title.asFormattedString()) / 2), 6.0F, 4210752); + font.draw(playerInventory.getDisplayName().asFormattedString(), 107.0F, (float) j, 4210752); + String spells = new TranslatableText("container.sorcerycraft.spells").getString(); + font.draw(spells, (float) (5 - font.getStringWidth(spells) / 2 + 48), 6.0F, 4210752); + renderXPCost(); + } + + @Override + protected void drawBackground(float delta, int mouseX, int mouseY) { + RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F); + assert minecraft != null; + minecraft.getTextureManager().bindTexture(TEXTURE); + int i = (width - containerWidth) / 2; + int j = (height - containerHeight) / 2; + blit(i, j, this.getBlitOffset(), 0.0F, 0.0F, containerWidth, containerHeight, 256, 512); + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + renderBackground(); + super.render(mouseX, mouseY, delta); + drawMouseoverTooltip(mouseX, mouseY); + + renderScrollbar(); + renderItems(); + + for (WidgetButtonPage button : buttons) { + if (button.isHovered()) { + button.renderToolTip(mouseX, mouseY); + } + button.visible = button.index < container.getRecipes().length; + if (button.visible) { + Spell spell = container.getRecipes()[button.getIndex() + indexStartOffset]; + button.setMessage(SpellTag.getTranslatedSpell(spell.getID(), spell.getLevel()).getString()); + button.setFocused((button.getIndex() + indexStartOffset) == selectedIndex); + } + } + } + + private void renderScrollbar() { + Spell[] spells = container.getRecipes(); + assert minecraft != null; + minecraft.getTextureManager().bindTexture(TEXTURE); + int i = (width - containerWidth) / 2; + int j = (height - containerHeight) / 2; + int k = spells.length + 1 - 7; + + if (k > 1) { + int l = 139 - (27 + (k - 1) * 139 / k); + int m = 1 + l / k + 139 / k; + int o = Math.min(113, indexStartOffset * m); + if (this.indexStartOffset == k - 1) { + o = 113; + } + + blit(i + 94, j + 18 + o, getBlitOffset(), 0.0F, 199.0F, 6, 27, 256, 512); + } else { + blit(i + 94, j + 18, getBlitOffset(), 6.0F, 199.0F, 6, 27, 256, 512); + } + } + + private void renderItems() { + int i = (width - containerWidth) / 2; + int j = (height - containerHeight) / 2; + int k = j + 16 + 1; + + Spell[] spells = container.getRecipes(); + for (int x = 0; x < spells.length; x++) { + if (!canScroll(spells.length) || (x >= indexStartOffset && x < 7 + indexStartOffset)) { + ItemStack itemStack = spells[x].getItemCost(); + itemRenderer.zOffset = 100.0F; + int n = k + 2; + + itemRenderer.renderGuiItem(itemStack, i + 5 + 68, n); + itemRenderer.renderGuiItemOverlay(font, itemStack, i + 5 + 68, n); + itemRenderer.zOffset = 0.0F; + k += 20; + } + } + } + + private void renderXPCost() { + if (container.getRecipes().length > 0) { + int i = container.getRecipes()[selectedIndex].getXPCost(); + int j = 8453920; + assert minecraft != null; + assert minecraft.player != null; + String string = new TranslatableText("container.repair.cost", i).getString(); + if (!container.canTakeResult(playerInventory.player)) { + j = 16736352; + } + + int x2 = containerWidth - 8; + int x1 = x2 - font.getStringWidth(string); + fill(x1, 65, x2, 77, 1325400064); + font.drawWithShadow(string, (float) x1, 67.0F, j); + } + } + + private boolean canScroll(int listSize) { + return listSize > 7; + } + + @Override + public boolean mouseScrolled(double d, double e, double amount) { + int i = container.getRecipes().length; + if (this.canScroll(i)) { + int j = i - 7; + indexStartOffset = (int) ((double) indexStartOffset - amount); + indexStartOffset = MathHelper.clamp(indexStartOffset, 0, j); + } + + return true; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + int i = container.getRecipes().length; + if (scrolling) { + int j = y + 18; + int k = j + 139; + int l = i - 7; + float f = ((float) mouseY - (float) j - 13.5F) / ((float) (k - j) - 27.0F); + f = f * (float)l + 0.5F; + indexStartOffset = MathHelper.clamp((int) f, 0, l); + return true; + } else { + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + scrolling = false; + int i = (width - containerWidth) / 2; + int j = (height - containerHeight) / 2; + if (this.canScroll(container.getRecipes().length) && mouseX > (double)(i + 94) && mouseX < (double) (i + 94 + 6) && mouseY > (double) (j + 18) && mouseY <= (double) (j + 18 + 139 + 1)) { + scrolling = true; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + private void syncRecipeIndex() { + container.setIndex(selectedIndex); + assert minecraft != null; + SelectSpellC2SPacket.send(minecraft, selectedIndex); + } + + private final WidgetButtonPage[] buttons = new WidgetButtonPage[7]; + + @Override + protected void init() { + super.init(); + int i = (width - containerWidth) / 2; + int j = (height - containerHeight) / 2; + int k = j + 16 + 2; + + for (int l = 0; l < 7; ++l) { + buttons[l] = addButton(new WidgetButtonPage(i + 5, k, l, (buttonWidget) -> { + selectedIndex = ((WidgetButtonPage) buttonWidget).getIndex() + indexStartOffset; + syncRecipeIndex(); + })); + k += 20; + } + } + + @Environment(EnvType.CLIENT) + private class WidgetButtonPage extends ButtonWidget { + final int index; + + public WidgetButtonPage(int i, int j, int k, PressAction pressAction) { + super(i, j, 89, 20, "", pressAction); + index = k; + visible = false; + } + + @Override + public void setFocused(boolean state) { + super.setFocused(state); + } + + public int getIndex() { + return this.index; + } + + public void renderToolTip(int mouseX, int mouseY) { + if (isHovered && container.getRecipes().length > index + indexStartOffset && mouseX > this.x + 65) { + ItemStack itemStack = container.getRecipes()[index + indexStartOffset].getItemCost(); + if (!itemStack.isEmpty()) { + renderTooltip(itemStack, mouseX, mouseY); + } + } + } + + @Override + public void drawCenteredString(TextRenderer textRenderer, String str, int ignored, int y, int color) { + drawString(textRenderer, str, x + 5, y, color); + } + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/client/entity/SpellEntityRenderer.java b/src/main/java/com/thebrokenrail/sorcerycraft/client/entity/SpellEntityRenderer.java new file mode 100644 index 0000000..63e7527 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/client/entity/SpellEntityRenderer.java @@ -0,0 +1,21 @@ +package com.thebrokenrail.sorcerycraft.client.entity; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.container.PlayerContainer; +import net.minecraft.entity.AreaEffectCloudEntity; +import net.minecraft.util.Identifier; + +@Environment(EnvType.CLIENT) +public class SpellEntityRenderer extends EntityRenderer { + public SpellEntityRenderer(EntityRenderDispatcher entityRenderDispatcher) { + super(entityRenderDispatcher); + } + + @Override + public Identifier getTexture(AreaEffectCloudEntity areaEffectCloudEntity) { + return PlayerContainer.BLOCK_ATLAS_TEXTURE; + } +} \ No newline at end of file diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/command/SpellCommand.java b/src/main/java/com/thebrokenrail/sorcerycraft/command/SpellCommand.java new file mode 100644 index 0000000..8de8e6d --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/command/SpellCommand.java @@ -0,0 +1,77 @@ +package com.thebrokenrail.sorcerycraft.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.thebrokenrail.sorcerycraft.spell.SpellPlayerEntity; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import net.minecraft.command.arguments.EntityArgumentType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class SpellCommand { + public static SuggestionProvider getOptions() { + return (ctx, builder) -> getSuggestionsBuilder(builder, new String[]{"clear", "list"}); + } + + private static CompletableFuture getSuggestionsBuilder(SuggestionsBuilder builder, String[] list) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + + if (list.length < 1) { + return Suggestions.empty(); + } + + for (String str : list) { + if (str.toLowerCase(Locale.ROOT).startsWith(remaining)) { + builder.suggest(str); + } + } + return builder.buildFuture(); + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(CommandManager.literal("spell") + .requires(source -> source.hasPermissionLevel(4)) + .then(CommandManager.argument("action", StringArgumentType.word()) + .suggests(getOptions()) + .then(CommandManager.argument("player", EntityArgumentType.player()) + .executes(ctx -> { + String action = StringArgumentType.getString(ctx, "action"); + PlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); + switch (action) { + case "clear": { + SpellPlayerEntity spellPlayer = (SpellPlayerEntity) player; + spellPlayer.setSpells(new HashMap<>()); + ctx.getSource().sendFeedback(new TranslatableText("command.sorcerycraft.spell.cleared_spells", player.getDisplayName()), true); + return 1; + } + case "list": { + ctx.getSource().sendFeedback(new TranslatableText("command.sorcerycraft.spell.listing_spells", player.getDisplayName()), false); + SpellPlayerEntity spellPlayer = (SpellPlayerEntity) player; + Map spells = spellPlayer.getSpells(); + for (Map.Entry entry : spells.entrySet()) { + ctx.getSource().sendFeedback(SpellTag.getTranslatedSpell(entry.getKey(), entry.getValue()).formatted(Formatting.YELLOW), false); + } + return 0; + } + default: { + ctx.getSource().sendFeedback(new TranslatableText("command.sorcerycraft.spell.invalid_action", action).formatted(Formatting.RED), true); + return -1; + } + } + }) + ) + ) + ); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/entity/SpellEntity.java b/src/main/java/com/thebrokenrail/sorcerycraft/entity/SpellEntity.java new file mode 100644 index 0000000..21bd130 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/entity/SpellEntity.java @@ -0,0 +1,91 @@ +package com.thebrokenrail.sorcerycraft.entity; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import com.thebrokenrail.sorcerycraft.spell.Spell; +import com.thebrokenrail.sorcerycraft.spell.SpellRegistry; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.entity.*; +import net.minecraft.entity.thrown.ThrownItemEntity; +import net.minecraft.item.Item; +import net.minecraft.network.Packet; +import net.minecraft.network.packet.s2c.play.EntitySpawnS2CPacket; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.world.World; + +import java.util.Map; + +public class SpellEntity extends ThrownItemEntity { + public SpellEntity(EntityType entityType, World world) { + super(entityType, world); + } + + public SpellEntity(World world, LivingEntity owner) { + super(SorceryCraft.SPELL_ENTITY, owner, world); + } + + public SpellEntity(World world, double x, double y, double z) { + super(SorceryCraft.SPELL_ENTITY, x, y, z, world); + } + + @Override + protected void onCollision(HitResult hitResult) { + boolean remove = false; + if (hitResult.getType() == HitResult.Type.BLOCK) { + remove = true; + } else if (hitResult.getType() == HitResult.Type.ENTITY) { + Entity entity = ((EntityHitResult) hitResult).getEntity(); + + Map spells = SpellTag.getSpells(getItem()); + + for (Map.Entry entry : spells.entrySet()) { + Spell spell = SpellRegistry.getSpell(entry.getKey(), entry.getValue()); + if (spell != null) { + if (Math.random() > 0.1 || spells.containsKey("steadfast_spell")) { + spell.execute(entity, this, getOwner()); + } else if (getOwner() != null) { + getOwner().playSound(SoundEvents.ENCHANT_THORNS_HIT, 1.0f, 1.0f); + spell.execute(getOwner(), this, getOwner()); + } + } + } + + remove = true; + } + if (remove && !getEntityWorld().isClient()) { + remove(); + } + } + + @Override + @Environment(EnvType.CLIENT) + public void handleStatus(byte status) { + if (status == 0) { + for (int i = 0; i < 12; i++) { + getEntityWorld().addParticle(ParticleTypes.WITCH, getX(), getY(), getZ(), 0.0d, 0.0d, 0.0d); + } + } + } + + @Override + public void tick() { + super.tick(); + if (!getEntityWorld().isClient()) { + getEntityWorld().sendEntityStatus(this, (byte) 0); + } + } + + @Override + protected Item getDefaultItem() { + return SorceryCraft.SPELL_ITEM; + } + + @Override + public Packet createSpawnPacket() { + return new EntitySpawnS2CPacket(this); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/item/SpellItem.java b/src/main/java/com/thebrokenrail/sorcerycraft/item/SpellItem.java new file mode 100644 index 0000000..faaa6d6 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/item/SpellItem.java @@ -0,0 +1,126 @@ +package com.thebrokenrail.sorcerycraft.item; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import com.thebrokenrail.sorcerycraft.entity.SpellEntity; +import com.thebrokenrail.sorcerycraft.packet.LearnedNewSpellS2CPacket; +import com.thebrokenrail.sorcerycraft.spell.*; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.*; +import net.minecraft.world.World; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SpellItem extends Item { + public SpellItem() { + super(new Item.Settings().group(SorceryCraft.ITEM_GROUP).rarity(Rarity.UNCOMMON).maxCount(16)); + } + + @Override + public TypedActionResult use(World world, PlayerEntity playerEntity, Hand hand) { + ItemStack itemStack = playerEntity.getStackInHand(hand); + + Map spells = SpellTag.getSpells(itemStack); + + if (spells.size() > 0) { + playerEntity.playSound(SoundEvents.BLOCK_ENCHANTMENT_TABLE_USE, 1.0f, 1.0f); + + if (!world.isClient()) { + SpellEntity entity = new SpellEntity(world, playerEntity); + entity.setItem(itemStack); + entity.setProperties(playerEntity, playerEntity.pitch, playerEntity.yaw, 0.0f, 1.5f, 1.0f); + world.spawnEntity(entity); + } + + if (!playerEntity.isCreative()) { + itemStack.decrement(1); + } + + return new TypedActionResult<>(ActionResult.SUCCESS, itemStack); + } else { + return new TypedActionResult<>(ActionResult.FAIL, itemStack); + } + } + + @Override + public boolean hasEnchantmentGlint(ItemStack stack) { + Map spells = SpellTag.getSpells(stack); + return spells.size() > 0; + } + + @Override + public void appendTooltip(ItemStack itemStack, World world, List tooltip, TooltipContext tooltipContext) { + Map spells = SpellTag.getSpells(itemStack); + for (Map.Entry entry : spells.entrySet()) { + tooltip.add(SpellTag.getTranslatedSpell(entry.getKey(), entry.getValue())); + } + } + + @Override + public void appendStacks(ItemGroup group, DefaultedList stacks) { + if (isIn(group)) { + stacks.add(new ItemStack(this)); + Spell[] spells = SpellRegistry.getSpells(); + for (Spell value : spells) { + ItemStack item = new ItemStack(this); + Map spell = new HashMap<>(); + spell.put(value.getID(), value.getLevel()); + SpellTag.setSpells(item, spell); + stacks.add(item); + } + } + } + + @Override + public boolean isEnchantable(ItemStack stack) { + return false; + } + + @Override + public void inventoryTick(ItemStack stack, World world, Entity entity, int slot, boolean selected) { + super.inventoryTick(stack, world, entity, slot, selected); + if (!world.isClient() && entity instanceof PlayerEntity) { + PlayerEntity player = (PlayerEntity) entity; + SpellPlayerEntity spellPlayer = (SpellPlayerEntity) player; + + Map playerSpells = spellPlayer.getSpells(); + Map itemSpells = SpellTag.getSpells(player.inventory.getInvStack(slot)); + + boolean changed = false; + + for (Map.Entry entry : itemSpells.entrySet()) { + Spell spell = SpellRegistry.getSpell(entry.getKey(), entry.getValue()); + if (spell != null) { + if (spell.getLevel() >= spell.getMaxLevel()) { + spell = SpellRegistry.getSpell(entry.getKey(), spell.getMaxLevel() - 1); + } + assert spell != null; + if (!playerSpells.containsKey(spell.getID()) || playerSpells.get(spell.getID()) < spell.getLevel()) { + changed = true; + + playerSpells.put(spell.getID(), spell.getLevel()); + assert world.getServer() != null; + Text text = new LiteralText("[").append(SpellTag.getTranslatedSpell(spell.getID(), spell.getLevel()).getString()).append("]").formatted(Formatting.GREEN); + world.getServer().getPlayerManager().sendToAll(new TranslatableText("chat.sorcerycraft.new_spell", player.getDisplayName(), text)); + } + } + } + + if (changed) { + LearnedNewSpellS2CPacket.send((ServerPlayerEntity) player); + spellPlayer.setSpells(playerSpells); + } + } + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinClientPlayNetworkHandler.java b/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinClientPlayNetworkHandler.java new file mode 100644 index 0000000..52bea0e --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinClientPlayNetworkHandler.java @@ -0,0 +1,42 @@ +package com.thebrokenrail.sorcerycraft.mixin; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import com.thebrokenrail.sorcerycraft.entity.SpellEntity; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.network.packet.s2c.play.EntitySpawnS2CPacket; +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; + +@SuppressWarnings("unused") +@Mixin(ClientPlayNetworkHandler.class) +public class MixinClientPlayNetworkHandler { + @Shadow private ClientWorld world; + + @Inject(method = "onEntitySpawn", at = @At(value = "TAIL")) + public void onEntitySpawn(EntitySpawnS2CPacket packet, CallbackInfo callbackInfo) { + EntityType entityType = packet.getEntityTypeId(); + Entity entity = null; + + if (entityType == SorceryCraft.SPELL_ENTITY) { + entity = new SpellEntity(world, packet.getX(), packet.getY(), packet.getZ()); + } + + if (entity != null) { + double x = packet.getX(); + double y = packet.getY(); + double z = packet.getZ(); + entity.updateTrackedPosition(x, y, z); + entity.pitch = (float) (packet.getPitch() * 360) / 250F; + entity.yaw = (float) (packet.getYaw() * 360) / 250F; + entity.setEntityId(packet.getId()); + entity.setUuid(packet.getUuid()); + world.addEntity(packet.getId(), entity); + } + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinPlayerEntity.java b/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinPlayerEntity.java new file mode 100644 index 0000000..43e012a --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinPlayerEntity.java @@ -0,0 +1,38 @@ +package com.thebrokenrail.sorcerycraft.mixin; + +import com.thebrokenrail.sorcerycraft.spell.SpellPlayerEntity; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.CompoundTag; +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; + +import java.util.Map; + +@SuppressWarnings("unused") +@Mixin(PlayerEntity.class) +public class MixinPlayerEntity implements SpellPlayerEntity { + private Map spells; + + @Inject(at = @At("HEAD"), method = "readCustomDataFromTag") + public void readCustomDataFromTag(CompoundTag tag, CallbackInfo info) { + spells = SpellTag.getSpells(tag); + } + + @Inject(at = @At("HEAD"), method = "writeCustomDataToTag") + public void writeCustomDataToTag(CompoundTag tag, CallbackInfo info) { + tag.put(SpellTag.SPELL_TAG, SpellTag.createSpellsTag(spells)); + } + + @Override + public void setSpells(Map spells) { + this.spells = spells; + } + + @Override + public Map getSpells() { + return spells; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinServerPlayerEntity.java b/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinServerPlayerEntity.java new file mode 100644 index 0000000..c665fe0 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/mixin/MixinServerPlayerEntity.java @@ -0,0 +1,31 @@ +package com.thebrokenrail.sorcerycraft.mixin; + +import com.thebrokenrail.sorcerycraft.packet.UpdateKnownSpellsS2CPacket; +import com.thebrokenrail.sorcerycraft.spell.SpellPlayerEntity; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.network.ServerPlayerEntity; +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; + +@SuppressWarnings("unused") +@Mixin(ServerPlayerEntity.class) +public abstract class MixinServerPlayerEntity implements SpellPlayerEntity { + @Inject(at = @At("HEAD"), method = "copyFrom") + public void copyFrom(ServerPlayerEntity oldPlayer, boolean alive, CallbackInfo ci) { + SpellPlayerEntity oldSpellPlayer = (SpellPlayerEntity) oldPlayer; + SpellPlayerEntity newSpellPlayer = this; + + newSpellPlayer.setSpells(oldSpellPlayer.getSpells()); + } + + @Inject(at = @At("HEAD"), method = "playerTick") + public void playerTick(CallbackInfo ignored) { + CompoundTag tag = new CompoundTag(); + tag.put(SpellTag.SPELL_TAG, SpellTag.createSpellsTag(getSpells())); + //noinspection ConstantConditions + UpdateKnownSpellsS2CPacket.send((ServerPlayerEntity) (Object) this, tag); + } +} \ No newline at end of file diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/packet/LearnedNewSpellS2CPacket.java b/src/main/java/com/thebrokenrail/sorcerycraft/packet/LearnedNewSpellS2CPacket.java new file mode 100644 index 0000000..1c56218 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/packet/LearnedNewSpellS2CPacket.java @@ -0,0 +1,24 @@ +package com.thebrokenrail.sorcerycraft.packet; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import io.netty.buffer.Unpooled; +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.Identifier; +import net.minecraft.util.PacketByteBuf; + +public class LearnedNewSpellS2CPacket { + @SuppressWarnings("unused") + public static void handle(PacketContext context, PacketByteBuf bytes) { + if (context.getPlayer() != null) { + context.getPlayer().playSound(SoundEvents.BLOCK_ENCHANTMENT_TABLE_USE, 1.0f, 1.0f); + } + } + + public static void send(ServerPlayerEntity player) { + PacketByteBuf bytes = new PacketByteBuf(Unpooled.buffer()); + player.networkHandler.sendPacket(new CustomPayloadS2CPacket(new Identifier(SorceryCraft.NAMESPACE, "learned_new_spell"), bytes)); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/packet/SelectSpellC2SPacket.java b/src/main/java/com/thebrokenrail/sorcerycraft/packet/SelectSpellC2SPacket.java new file mode 100644 index 0000000..b2334a2 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/packet/SelectSpellC2SPacket.java @@ -0,0 +1,29 @@ +package com.thebrokenrail.sorcerycraft.packet; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import com.thebrokenrail.sorcerycraft.block.CastingTableContainer; +import io.netty.buffer.Unpooled; +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.container.Container; +import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; +import net.minecraft.util.Identifier; +import net.minecraft.util.PacketByteBuf; + +public class SelectSpellC2SPacket { + public static void handle(PacketContext context, PacketByteBuf bytes) { + int index = bytes.readInt(); + Container container = context.getPlayer().container; + if (container instanceof CastingTableContainer) { + CastingTableContainer merchantContainer = (CastingTableContainer) container; + merchantContainer.setIndex(index); + } + } + + public static void send(MinecraftClient minecraft, int index) { + PacketByteBuf bytes = new PacketByteBuf(Unpooled.buffer()); + bytes.writeInt(index); + assert minecraft.getNetworkHandler() != null; + minecraft.getNetworkHandler().sendPacket(new CustomPayloadC2SPacket(new Identifier(SorceryCraft.NAMESPACE, "select_spell"), bytes)); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/packet/UpdateKnownSpellsS2CPacket.java b/src/main/java/com/thebrokenrail/sorcerycraft/packet/UpdateKnownSpellsS2CPacket.java new file mode 100644 index 0000000..b92c088 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/packet/UpdateKnownSpellsS2CPacket.java @@ -0,0 +1,28 @@ +package com.thebrokenrail.sorcerycraft.packet; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import com.thebrokenrail.sorcerycraft.spell.SpellPlayerEntity; +import com.thebrokenrail.sorcerycraft.spell.SpellTag; +import io.netty.buffer.Unpooled; +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.PacketByteBuf; + +public class UpdateKnownSpellsS2CPacket { + public static void handle(PacketContext context, PacketByteBuf bytes) { + CompoundTag tag = bytes.readCompoundTag(); + if (context.getPlayer() != null) { + SpellPlayerEntity spellPlayer = (SpellPlayerEntity) context.getPlayer(); + spellPlayer.setSpells(SpellTag.getSpells(tag)); + } + } + + public static void send(ServerPlayerEntity player, CompoundTag tag) { + PacketByteBuf bytes = new PacketByteBuf(Unpooled.buffer()); + bytes.writeCompoundTag(tag); + player.networkHandler.sendPacket(new CustomPayloadS2CPacket(new Identifier(SorceryCraft.NAMESPACE, "update_known_spells"), bytes)); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/DamageSpell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/DamageSpell.java new file mode 100644 index 0000000..9fb71bf --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/DamageSpell.java @@ -0,0 +1,51 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public class DamageSpell extends Spell { + public DamageSpell(String id, int level) { + super(id, level); + } + + @Override + public void execute(Entity target, Entity source, Entity attacker) { + if (target instanceof LivingEntity) { + StatusEffects.INSTANT_DAMAGE.applyInstantEffect(source, attacker, (LivingEntity) target, getLevel(), 1.0d); + } + } + + @Override + public int getXPCost() { + switch (getLevel()) { + case 0: { + return 5; + } + case 1: { + return 10; + } + } + return -1; + } + + @Override + public ItemStack getItemCost() { + switch (getLevel()) { + case 0: { + return ItemStack.EMPTY; + } + case 1: { + return new ItemStack(Items.WITHER_ROSE); + } + } + return ItemStack.EMPTY; + } + + @Override + public int getMaxLevel() { + return 2; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/DissolveSpell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/DissolveSpell.java new file mode 100644 index 0000000..ba0facf --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/DissolveSpell.java @@ -0,0 +1,34 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public class DissolveSpell extends Spell { + public DissolveSpell(String id, int level) { + super(id, level); + } + + @Override + public void execute(Entity target, Entity source, Entity attacker) { + if (target instanceof LivingEntity) { + ((LivingEntity) target).clearStatusEffects(); + } + } + + @Override + public int getXPCost() { + return 12; + } + + @Override + public ItemStack getItemCost() { + return new ItemStack(Items.CLOCK); + } + + @Override + public int getMaxLevel() { + return 1; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/FlameSpell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/FlameSpell.java new file mode 100644 index 0000000..cb534ae --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/FlameSpell.java @@ -0,0 +1,47 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public class FlameSpell extends Spell { + public FlameSpell(String id, int level) { + super(id, level); + } + + @Override + public void execute(Entity target, Entity source, Entity attacker) { + target.setFireTicks(400 + (getLevel() * 200)); + } + + @Override + public int getXPCost() { + switch (getLevel()) { + case 0: { + return 8; + } + case 1: { + return 16; + } + } + return -1; + } + + @Override + public ItemStack getItemCost() { + switch (getLevel()) { + case 0: { + return new ItemStack(Items.FLINT_AND_STEEL); + } + case 1: { + return new ItemStack(Items.FIRE_CHARGE); + } + } + return ItemStack.EMPTY; + } + + @Override + public int getMaxLevel() { + return 2; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/HealSpell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/HealSpell.java new file mode 100644 index 0000000..0f0122f --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/HealSpell.java @@ -0,0 +1,51 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public class HealSpell extends Spell { + public HealSpell(String id, int level) { + super(id, level); + } + + @Override + public void execute(Entity target, Entity source, Entity attacker) { + if (target instanceof LivingEntity) { + StatusEffects.INSTANT_HEALTH.applyInstantEffect(source, attacker, (LivingEntity) target, getLevel(), 1.0d); + } + } + + @Override + public int getXPCost() { + switch (getLevel()) { + case 0: { + return 5; + } + case 1: { + return 10; + } + } + return -1; + } + + @Override + public ItemStack getItemCost() { + switch (getLevel()) { + case 0: { + return ItemStack.EMPTY; + } + case 1: { + return new ItemStack(Items.GHAST_TEAR); + } + } + return ItemStack.EMPTY; + } + + @Override + public int getMaxLevel() { + return 2; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/LevitateSpell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/LevitateSpell.java new file mode 100644 index 0000000..5b6ea36 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/LevitateSpell.java @@ -0,0 +1,52 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public class LevitateSpell extends Spell { + public LevitateSpell(String id, int level) { + super(id, level); + } + + @Override + public void execute(Entity target, Entity source, Entity attacker) { + if (target instanceof LivingEntity) { + ((LivingEntity) target).addStatusEffect(new StatusEffectInstance(StatusEffects.LEVITATION, 400 + (getLevel() * 160))); + } + } + + @Override + public int getXPCost() { + switch (getLevel()) { + case 0: { + return 12; + } + case 1: { + return 21; + } + } + return -1; + } + + @Override + public ItemStack getItemCost() { + switch (getLevel()) { + case 0: { + return new ItemStack(Items.SHULKER_SHELL); + } + case 1: { + return new ItemStack(Items.SHULKER_SHELL, 4); + } + } + return ItemStack.EMPTY; + } + + @Override + public int getMaxLevel() { + return 2; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/RandomSpellLootTableFunction.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/RandomSpellLootTableFunction.java new file mode 100644 index 0000000..ef03f83 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/RandomSpellLootTableFunction.java @@ -0,0 +1,36 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.item.ItemStack; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.function.ConditionalLootFunction; +import net.minecraft.loot.function.LootFunction; + +public class RandomSpellLootTableFunction extends ConditionalLootFunction { + private RandomSpellLootTableFunction(LootCondition[] conditions) { + super(conditions); + } + + public ItemStack process(ItemStack stack, LootContext context) { + Spell[] spells = SpellRegistry.getSpells(); + int index = context.getRandom().nextInt(spells.length); + Map spell = new HashMap<>(); + spell.put(spells[index].getID(), spells[index].getLevel()); + SpellTag.setSpells(stack, spell); + return stack; + } + + public static class Builder extends ConditionalLootFunction.Builder { + @Override + protected RandomSpellLootTableFunction.Builder getThisBuilder() { + return this; + } + + public LootFunction build() { + return new RandomSpellLootTableFunction(getConditions()); + } + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/Spell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/Spell.java new file mode 100644 index 0000000..d8b33e9 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/Spell.java @@ -0,0 +1,30 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; + +public abstract class Spell { + private final String id; + private final int level; + + public Spell(String id, int level) { + this.id = id; + this.level = level; + } + + public String getID() { + return id; + } + + public int getLevel() { + return level; + } + + public abstract void execute(Entity target, Entity source, Entity attacker); + + public abstract int getXPCost(); + + public abstract ItemStack getItemCost(); + + public abstract int getMaxLevel(); +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellPlayerEntity.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellPlayerEntity.java new file mode 100644 index 0000000..3d29ade --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellPlayerEntity.java @@ -0,0 +1,9 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import java.util.Map; + +public interface SpellPlayerEntity { + void setSpells(Map spells); + + Map getSpells(); +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellRegistry.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellRegistry.java new file mode 100644 index 0000000..42fc731 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellRegistry.java @@ -0,0 +1,61 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SpellRegistry { + private static final Map> spells = new HashMap<>(); + + public static Spell getSpell(String id, int level) { + if (!spells.containsKey(id)) { + return null; + } + try { + return (Spell) spells.get(id).getConstructor(String.class, int.class).newInstance(id, level); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } + } + + public static int getMaxLevel(String id) { + Spell tempSpell = getSpell(id, 0); + if (tempSpell == null) { + return -1; + } + return tempSpell.getMaxLevel(); + } + + public static Spell[] getSpells() { + List out = new ArrayList<>(); + for (Map.Entry> entry : spells.entrySet()) { + int maxLevel = getMaxLevel(entry.getKey()); + if (maxLevel == -1) { + continue; + } + for (int i = 0; i < maxLevel; i++) { + Spell spell = getSpell(entry.getKey(), i); + if (spell != null) { + out.add(spell); + } + } + } + return out.toArray(new Spell[0]); + } + + private static void registerSpell(String id, Class spell) { + spells.put(id, spell); + } + + public static void init() { + SpellRegistry.registerSpell("heal_spell", HealSpell.class); + SpellRegistry.registerSpell("damage_spell", DamageSpell.class); + SpellRegistry.registerSpell("dissolve_spell", DissolveSpell.class); + SpellRegistry.registerSpell("steadfast_spell", SteadfastSpell.class); + SpellRegistry.registerSpell("flame_spell", FlameSpell.class); + SpellRegistry.registerSpell("levitate_spell", LevitateSpell.class); + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellTag.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellTag.java new file mode 100644 index 0000000..82ac865 --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SpellTag.java @@ -0,0 +1,85 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import com.thebrokenrail.sorcerycraft.SorceryCraft; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; + +import java.util.HashMap; +import java.util.Map; + +public class SpellTag { + public static final String SPELL_TAG = "Spells"; + + public static void setSpells(ItemStack itemStack, Map map) { + CompoundTag tag; + if (itemStack.hasTag()) { + tag = itemStack.getTag(); + } else { + tag = new CompoundTag(); + tag.put(SPELL_TAG, new ListTag()); + } + assert tag != null; + + tag.put(SPELL_TAG, createSpellsTag(map)); + + itemStack.setTag(tag); + } + + public static ListTag createSpellsTag(Map map) { + ListTag spells = new ListTag(); + + for (Map.Entry entry : map.entrySet()) { + CompoundTag spell = new CompoundTag(); + spell.putString("id", entry.getKey()); + spell.putInt("level", entry.getValue()); + spells.add(spell); + } + + return spells; + } + + public static Map getSpells(ItemStack itemStack) { + return getSpells(itemStack.getTag()); + } + + public static Map getSpells(CompoundTag tag) { + if (tag == null) { + tag = new CompoundTag(); + tag.put(SPELL_TAG, new ListTag()); + } + Tag spellsTag = tag.get(SPELL_TAG); + ListTag spells; + if (spellsTag instanceof ListTag) { + spells = (ListTag) spellsTag; + } else { + spells = new ListTag(); + } + + Map map = new HashMap<>(); + + for (int i = 0; i < spells.size(); i++) { + CompoundTag spell = spells.getCompound(i); + String id = spell.getString("id"); + int level = spell.getInt("level"); + if (map.get(id) == null || map.get(id) < level) { + map.put(id, level); + } + } + + return map; + } + + public static Text getTranslatedSpell(String id, int level) { + Text text = new TranslatableText("spell." + SorceryCraft.NAMESPACE + '.' + id); + text.formatted(Formatting.GRAY); + if (level != 0 || SpellRegistry.getMaxLevel(id) != 1) { + text.append(" ").append(new TranslatableText("enchantment.level." + (level + 1))); + } + return text; + } +} diff --git a/src/main/java/com/thebrokenrail/sorcerycraft/spell/SteadfastSpell.java b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SteadfastSpell.java new file mode 100644 index 0000000..3adb05b --- /dev/null +++ b/src/main/java/com/thebrokenrail/sorcerycraft/spell/SteadfastSpell.java @@ -0,0 +1,30 @@ +package com.thebrokenrail.sorcerycraft.spell; + +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public class SteadfastSpell extends Spell { + public SteadfastSpell(String id, int level) { + super(id, level); + } + + @Override + public void execute(Entity target, Entity source, Entity attacker) { + } + + @Override + public int getXPCost() { + return 18; + } + + @Override + public ItemStack getItemCost() { + return new ItemStack(Items.NETHERITE_INGOT); + } + + @Override + public int getMaxLevel() { + return 1; + } +} diff --git a/src/main/resources/assets/sorcerycraft/blockstates/casting_table.json b/src/main/resources/assets/sorcerycraft/blockstates/casting_table.json new file mode 100644 index 0000000..a500e76 --- /dev/null +++ b/src/main/resources/assets/sorcerycraft/blockstates/casting_table.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "sorcerycraft:block/casting_table" } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/sorcerycraft/lang/en_us.json b/src/main/resources/assets/sorcerycraft/lang/en_us.json new file mode 100644 index 0000000..4d8d77b --- /dev/null +++ b/src/main/resources/assets/sorcerycraft/lang/en_us.json @@ -0,0 +1,18 @@ +{ + "item.sorcerycraft.spell": "Spell", + "block.sorcerycraft.casting_table": "Casting Table", + "itemGroup.sorcerycraft.spells": "SorceryCraft", + "container.sorcerycraft.casting_table": "Casting Table", + "container.sorcerycraft.spells": "Spells", + "chat.sorcerycraft.new_spell": "%s has learned the spell %s", + "entity.sorcerycraft.spell": "Spell", + "command.sorcerycraft.spell.invalid_action": "Invalid sub-command \"%s\"", + "command.sorcerycraft.spell.cleared_spells": "All known spells cleared from %s", + "command.sorcerycraft.spell.listing_spells": "%s knows the following spells:", + "spell.sorcerycraft.damage_spell": "Damage", + "spell.sorcerycraft.heal_spell": "Heal", + "spell.sorcerycraft.dissolve_spell": "Dissolve", + "spell.sorcerycraft.steadfast_spell": "Steadfast", + "spell.sorcerycraft.flame_spell": "Flame", + "spell.sorcerycraft.levitate_spell": "Levitate" +} \ No newline at end of file diff --git a/src/main/resources/assets/sorcerycraft/models/block/casting_table.json b/src/main/resources/assets/sorcerycraft/models/block/casting_table.json new file mode 100644 index 0000000..ab5ad03 --- /dev/null +++ b/src/main/resources/assets/sorcerycraft/models/block/casting_table.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "sorcerycraft:block/casting_table" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/sorcerycraft/models/item/casting_table.json b/src/main/resources/assets/sorcerycraft/models/item/casting_table.json new file mode 100644 index 0000000..db24271 --- /dev/null +++ b/src/main/resources/assets/sorcerycraft/models/item/casting_table.json @@ -0,0 +1,3 @@ +{ + "parent": "sorcerycraft:block/casting_table" +} \ No newline at end of file diff --git a/src/main/resources/assets/sorcerycraft/models/item/spell.json b/src/main/resources/assets/sorcerycraft/models/item/spell.json new file mode 100644 index 0000000..e595ffe --- /dev/null +++ b/src/main/resources/assets/sorcerycraft/models/item/spell.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "minecraft:item/paper" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/sorcerycraft/textures/block/casting_table.png b/src/main/resources/assets/sorcerycraft/textures/block/casting_table.png new file mode 100644 index 0000000..a68913a Binary files /dev/null and b/src/main/resources/assets/sorcerycraft/textures/block/casting_table.png differ diff --git a/src/main/resources/data/sorcerycraft/loot_tables/blocks/casting_table.json b/src/main/resources/data/sorcerycraft/loot_tables/blocks/casting_table.json new file mode 100644 index 0000000..96e9030 --- /dev/null +++ b/src/main/resources/data/sorcerycraft/loot_tables/blocks/casting_table.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "sorcerycraft:casting_table" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/data/sorcerycraft/recipes/casting_table.json b/src/main/resources/data/sorcerycraft/recipes/casting_table.json new file mode 100644 index 0000000..013913b --- /dev/null +++ b/src/main/resources/data/sorcerycraft/recipes/casting_table.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "LCL", + "CDC", + "LCL" + ], + "key": { + "L": { + "item": "minecraft:lapis_lazuli" + }, + "C": { + "item": "minecraft:cobblestone" + }, + "D": { + "item": "minecraft:diamond" + } + }, + "result": { + "item": "sorcerycraft:casting_table", + "count": 1 + } +} + diff --git a/src/main/resources/data/sorcerycraft/recipes/spell.json b/src/main/resources/data/sorcerycraft/recipes/spell.json new file mode 100644 index 0000000..939be77 --- /dev/null +++ b/src/main/resources/data/sorcerycraft/recipes/spell.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + " L ", + "LPL", + " L " + ], + "key": { + "L": { + "item": "minecraft:lapis_lazuli" + }, + "P": { + "item": "minecraft:paper" + } + }, + "result": { + "item": "sorcerycraft:spell", + "count": 1 + } +} + diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..a24e790 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "sorcerycraft", + "version": "${version}", + "name": "SorceryCraft", + "description": "Cast Spells in Minecraft!", + "authors": [ + "TheBrokenRail" + ], + "contact": { + "homepage": "https://thebrokenrail.com/", + "sources": "https://gitea.thebrokenrail.com/TheBrokenRail/SorceryCraft.git" + }, + "license": "MIT", + "icon": "assets/sorcerycraft/textures/block/casting_table.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.thebrokenrail.sorcerycraft.SorceryCraft" + ], + "client": [ + "com.thebrokenrail.sorcerycraft.SorceryCraft" + ] + }, + "mixins": [ + "sorcerycraft.mixins.json" + ], + "depends": { + "fabricloader": ">=0.7.4", + "fabric": "*", + "minecraft": "1.16.x" + } +} diff --git a/src/main/resources/sorcerycraft.mixins.json b/src/main/resources/sorcerycraft.mixins.json new file mode 100644 index 0000000..4166340 --- /dev/null +++ b/src/main/resources/sorcerycraft.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "package": "com.thebrokenrail.sorcerycraft.mixin", + "compatibilityLevel": "JAVA_8", + "client": [ + "MixinClientPlayNetworkHandler" + ], + "mixins": [ + "MixinPlayerEntity", + "MixinServerPlayerEntity" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file