├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── dev │ └── gegy │ └── magic │ ├── Magic.java │ ├── MagicClient.java │ ├── casting │ ├── ServerCasting.java │ ├── ServerCastingBuilder.java │ ├── ServerCastingSource.java │ ├── ServerCastingTracker.java │ ├── drawing │ │ ├── DrawingGlyphParameters.java │ │ ├── DrawingParameters.java │ │ ├── ServerCastingDrawing.java │ │ ├── ServerDrawingGlyph.java │ │ └── event │ │ │ ├── ClientDrawingEventSenders.java │ │ │ ├── ServerDrawingEventSenders.java │ │ │ ├── c2s │ │ │ ├── BeginGlyphC2SEvent.java │ │ │ ├── CancelGlyphC2SEvent.java │ │ │ ├── DrawGlyphShapeC2SEvent.java │ │ │ ├── DrawGlyphStrokeC2SEvent.java │ │ │ └── PrepareSpellC2SEvent.java │ │ │ └── s2c │ │ │ ├── CancelDrawingS2CEvent.java │ │ │ ├── DrawGlyphS2CEvent.java │ │ │ └── UpdateDrawingS2CEvent.java │ ├── event │ │ ├── CastingEventSpec.java │ │ ├── EventSenderFactory.java │ │ └── InboundCastingEvent.java │ └── spell │ │ ├── SpellParameters.java │ │ ├── beam │ │ ├── BeamParameters.java │ │ ├── ServerCastingBeam.java │ │ └── SetBeamActive.java │ │ └── teleport │ │ ├── SelectTeleportTarget.java │ │ ├── ServerCastingTeleport.java │ │ ├── TeleportInput.java │ │ ├── TeleportParameters.java │ │ ├── TeleportTarget.java │ │ └── TeleportTargetSymbol.java │ ├── client │ ├── animator │ │ ├── ArmPose.java │ │ ├── CastingAnimatableEntity.java │ │ ├── CastingAnimator.java │ │ └── CastingPose.java │ ├── casting │ │ ├── ClientCasting.java │ │ ├── ClientCastingBuilder.java │ │ ├── ClientCastingSource.java │ │ ├── ClientCastingTracker.java │ │ ├── ClientCastingType.java │ │ ├── ConfiguredClientCasting.java │ │ ├── blend │ │ │ ├── CastingBlendBuilder.java │ │ │ ├── CastingBlendType.java │ │ │ └── CastingBlender.java │ │ ├── drawing │ │ │ ├── ClientCastingDrawing.java │ │ │ ├── ClientDrawingGlyph.java │ │ │ ├── FadingGlyph.java │ │ │ ├── GlyphStrokeTracker.java │ │ │ ├── input │ │ │ │ ├── BeginDraw.java │ │ │ │ ├── ContinueDraw.java │ │ │ │ ├── DrawGlyph.java │ │ │ │ ├── DrawingCastingInput.java │ │ │ │ ├── DrawingInputState.java │ │ │ │ └── outline │ │ │ │ │ ├── GlyphOutline.java │ │ │ │ │ ├── GlyphOutlineSolver.java │ │ │ │ │ └── GlyphOutlineTracker.java │ │ │ └── preparing │ │ │ │ └── ClientCastingPreparing.java │ │ └── spell │ │ │ ├── beam │ │ │ └── ClientCastingBeam.java │ │ │ └── teleport │ │ │ └── ClientCastingTeleport.java │ ├── effect │ │ ├── Effect.java │ │ ├── EffectManager.java │ │ ├── EffectMap.java │ │ ├── EffectSelector.java │ │ ├── EffectSystem.java │ │ ├── EffectType.java │ │ ├── GlobalEffectList.java │ │ ├── casting │ │ │ ├── drawing │ │ │ │ └── DrawingEffect.java │ │ │ └── spell │ │ │ │ ├── PreparedSpellEffect.java │ │ │ │ ├── SpellEffects.java │ │ │ │ ├── beam │ │ │ │ ├── BeamEffect.java │ │ │ │ ├── BeamEffectSystem.java │ │ │ │ └── render │ │ │ │ │ ├── BeamEffectRenderer.java │ │ │ │ │ ├── BeamEndWorldShader.java │ │ │ │ │ ├── BeamRenderParameters.java │ │ │ │ │ ├── BeamTexture.java │ │ │ │ │ ├── BeamTextureShader.java │ │ │ │ │ └── BeamWorldShader.java │ │ │ │ └── teleport │ │ │ │ ├── TeleportEffect.java │ │ │ │ ├── TeleportEffectRenderer.java │ │ │ │ ├── TeleportEffectSystem.java │ │ │ │ └── TeleportTargetAnimator.java │ │ ├── glyph │ │ │ ├── GlyphEffectRenderer.java │ │ │ ├── GlyphEffectSystem.java │ │ │ ├── GlyphRenderParameters.java │ │ │ ├── GlyphTexture.java │ │ │ ├── GlyphTextureShader.java │ │ │ ├── GlyphWorldShader.java │ │ │ └── GlyphsEffect.java │ │ └── shader │ │ │ ├── EffectShader.java │ │ │ ├── EffectShaderProgram.java │ │ │ └── EffectTexture.java │ ├── event │ │ └── ClientRemoveEntityEvent.java │ ├── glyph │ │ ├── GlyphPlane.java │ │ ├── GlyphStroke.java │ │ ├── SpellSource.java │ │ ├── spell │ │ │ ├── Spell.java │ │ │ ├── SpellCasting.java │ │ │ ├── SpellCastingGlyph.java │ │ │ ├── SpellGlyphs.java │ │ │ ├── SpellPrepareBlender.java │ │ │ └── transform │ │ │ │ ├── SpellTransform.java │ │ │ │ ├── SpellTransformType.java │ │ │ │ ├── StaticSpellTransform.java │ │ │ │ └── TrackingSpellTransform.java │ │ └── transform │ │ │ ├── BlendingGlyphTransform.java │ │ │ └── GlyphTransform.java │ ├── particle │ │ ├── MagicParticleFactories.java │ │ └── SparkParticle.java │ └── render │ │ ├── GeometryBuilder.java │ │ └── gl │ │ ├── GlBindableObject.java │ │ ├── GlBinding.java │ │ ├── GlBuffer.java │ │ ├── GlGeometry.java │ │ ├── GlObject.java │ │ └── GlVertexArray.java │ ├── event │ ├── LateTrackingEvent.java │ └── PlayerLeaveEvent.java │ ├── glyph │ ├── GlyphForm.java │ ├── GlyphStyle.java │ ├── GlyphType.java │ └── shape │ │ ├── GlyphDebugRenderer.java │ │ ├── GlyphEdge.java │ │ ├── GlyphNode.java │ │ ├── GlyphShape.java │ │ ├── GlyphShapeGenerator.java │ │ └── GlyphShapeStorage.java │ ├── math │ ├── AnimatedColor.java │ ├── AnimationTimer.java │ ├── ColorHsluv.java │ ├── ColorRgb.java │ └── Easings.java │ ├── mixin │ ├── ServerEntityMixin.java │ └── client │ │ ├── ClientEntityCallbacksMixin.java │ │ ├── PlayerMixin.java │ │ └── PlayerModelMixin.java │ ├── network │ ├── NetworkAddressing.java │ ├── NetworkSender.java │ ├── c2s │ │ ├── CastingEventC2SPacket.java │ │ └── MagicC2SNetworking.java │ ├── codec │ │ ├── PacketCodec.java │ │ ├── PacketDecoder.java │ │ └── PacketEncoder.java │ └── s2c │ │ ├── CastingEventS2CPacket.java │ │ ├── MagicS2CNetworking.java │ │ └── SetCastingS2CPacket.java │ └── particle │ └── MagicParticles.java └── resources ├── assets └── magic │ ├── particles │ └── spark.json │ ├── shaders │ ├── beam │ │ ├── cloud_texture.fsh │ │ ├── end_texture.vsh │ │ ├── end_world.vsh │ │ ├── impact_texture.fsh │ │ ├── texture.fsh │ │ ├── texture.vsh │ │ └── world.vsh │ ├── effect_world.fsh │ ├── glyph │ │ ├── texture.fsh │ │ ├── texture.vsh │ │ └── world.vsh │ └── include │ │ ├── beam_color.glsl │ │ └── noise.glsl │ └── textures │ └── particle │ └── spark.png ├── fabric.mod.json └── magic.mixins.json /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # fabric 28 | 29 | run/ 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unnamed Magic Mod 2 | This is a heavily work-in-progress mod that intends to introduce intuitive, immersive and composable spellcasting to Minecraft. 3 | Currently, only the spell drawing mechanics are implemented. 4 | 5 | ![Video demonstration of the mod's current features](https://i.imgur.com/YKIkRHI.gif) 6 | 7 | _An example of the spellcasting animation in its current state_ 8 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '1.0.+' 3 | id 'maven-publish' 4 | } 5 | 6 | sourceCompatibility = JavaVersion.VERSION_17 7 | targetCompatibility = JavaVersion.VERSION_17 8 | 9 | archivesBaseName = project.archives_base_name 10 | version = project.mod_version 11 | group = project.maven_group 12 | 13 | repositories { 14 | mavenCentral() 15 | maven { url = "https://maven.gegy.dev/" } 16 | maven { url = "https://api.modrinth.com/maven/" } 17 | } 18 | 19 | dependencies { 20 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 21 | mappings loom.officialMojangMappings() 22 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 23 | 24 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 25 | 26 | implementation include('org.hsluv:hsluv:0.2') 27 | } 28 | 29 | processResources { 30 | inputs.property "version", project.version 31 | 32 | filesMatching("fabric.mod.json") { 33 | expand "version": project.version 34 | } 35 | } 36 | 37 | tasks.withType(JavaCompile).configureEach { 38 | it.options.encoding = "UTF-8" 39 | it.options.release = 17 40 | } 41 | 42 | java { 43 | withSourcesJar() 44 | } 45 | 46 | jar { 47 | from("LICENSE") { 48 | rename { "${it}_${project.archivesBaseName}" } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | minecraft_version=1.20-rc1 6 | loader_version=0.14.21 7 | 8 | # Dependencies 9 | fabric_version=0.83.0+1.20 10 | 11 | # Mod Properties 12 | mod_version=1.0.0 13 | maven_group=dev.gegy 14 | archives_base_name=magic 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gegy/magic/0529d0f7b5a2edc1d76e3c9b638d573b65e6428b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/Magic.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic; 2 | 3 | import dev.gegy.magic.casting.ServerCastingTracker; 4 | import dev.gegy.magic.glyph.GlyphType; 5 | import dev.gegy.magic.network.c2s.MagicC2SNetworking; 6 | import dev.gegy.magic.particle.MagicParticles; 7 | import net.fabricmc.api.ModInitializer; 8 | import net.minecraft.resources.ResourceLocation; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.Logger; 11 | 12 | public final class Magic implements ModInitializer { 13 | public static final String ID = "magic"; 14 | public static final Logger LOGGER = LogManager.getLogger(ID); 15 | 16 | @Override 17 | public void onInitialize() { 18 | MagicC2SNetworking.registerReceivers(); 19 | 20 | MagicParticles.register(); 21 | 22 | ServerCastingTracker.register(); 23 | GlyphType.onInitialize(); 24 | } 25 | 26 | public static ResourceLocation identifier(final String id) { 27 | return new ResourceLocation(ID, id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/MagicClient.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic; 2 | 3 | import dev.gegy.magic.client.casting.ClientCastingTracker; 4 | import dev.gegy.magic.client.casting.ClientCastingType; 5 | import dev.gegy.magic.client.effect.EffectManager; 6 | import dev.gegy.magic.client.particle.MagicParticleFactories; 7 | import dev.gegy.magic.network.s2c.MagicS2CNetworking; 8 | import net.fabricmc.api.ClientModInitializer; 9 | 10 | public final class MagicClient implements ClientModInitializer { 11 | @Override 12 | public void onInitializeClient() { 13 | EffectManager.onInitialize(); 14 | ClientCastingType.onInitialize(); 15 | 16 | MagicS2CNetworking.registerReceivers(); 17 | 18 | MagicParticleFactories.register(); 19 | 20 | ClientCastingTracker.register(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/ServerCasting.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting; 2 | 3 | import dev.gegy.magic.client.casting.ConfiguredClientCasting; 4 | import net.minecraft.network.FriendlyByteBuf; 5 | import net.minecraft.resources.ResourceLocation; 6 | import net.minecraft.server.level.ServerPlayer; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | public interface ServerCasting { 10 | @Nullable 11 | ServerCasting.Factory tick(); 12 | 13 | void handleEvent(ResourceLocation id, FriendlyByteBuf buf); 14 | 15 | @Nullable 16 | ConfiguredClientCasting createClientCasting(); 17 | 18 | interface Factory { 19 | ServerCasting build(ServerPlayer player, ServerCastingBuilder casting); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/ServerCastingSource.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting; 2 | 3 | import dev.gegy.magic.casting.event.CastingEventSpec; 4 | import dev.gegy.magic.casting.event.EventSenderFactory; 5 | import dev.gegy.magic.client.casting.ConfiguredClientCasting; 6 | import dev.gegy.magic.network.NetworkAddressing; 7 | import dev.gegy.magic.network.NetworkSender; 8 | import dev.gegy.magic.network.s2c.CastingEventS2CPacket; 9 | import dev.gegy.magic.network.s2c.SetCastingS2CPacket; 10 | import net.minecraft.network.FriendlyByteBuf; 11 | import net.minecraft.resources.ResourceLocation; 12 | import net.minecraft.server.level.ServerPlayer; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | public final class ServerCastingSource implements AutoCloseable { 16 | private final ServerPlayer player; 17 | private ServerCasting casting; 18 | 19 | private final NetworkAddressing addressing; 20 | 21 | public ServerCastingSource(final ServerPlayer player) { 22 | this.player = player; 23 | 24 | addressing = NetworkAddressing.trackingClients(player); 25 | } 26 | 27 | public ServerPlayer player() { 28 | return player; 29 | } 30 | 31 | public void setCasting(@Nullable final ServerCasting.Factory factory) { 32 | if (factory != null) { 33 | final ServerCasting casting = factory.build(player, createCastingBuilder()); 34 | this.casting = casting; 35 | notifyClientCasting(casting.createClientCasting()); 36 | } else { 37 | casting = null; 38 | notifyClientCasting(null); 39 | } 40 | } 41 | 42 | private ServerCastingBuilder createCastingBuilder() { 43 | final NetworkAddressing addressing = this.addressing; 44 | final ServerPlayer player = this.player; 45 | 46 | final NetworkSender eventSender = addressing.sender(CastingEventS2CPacket::sendTo); 47 | return new ServerCastingBuilder(new EventSenderFactory() { 48 | @Override 49 | public NetworkSender create(final CastingEventSpec spec) { 50 | return eventSender.map(event -> CastingEventS2CPacket.create(player, spec, event)); 51 | } 52 | }); 53 | } 54 | 55 | private void notifyClientCasting(@Nullable final ConfiguredClientCasting clientCasting) { 56 | addressing.sender(SetCastingS2CPacket::sendTo) 57 | .broadcastAndSend(SetCastingS2CPacket.create(player, clientCasting)); 58 | } 59 | 60 | public void tick() { 61 | final ServerCasting casting = this.casting; 62 | if (casting != null) { 63 | tickCasting(casting); 64 | } 65 | } 66 | 67 | private void tickCasting(final ServerCasting casting) { 68 | final ServerCasting.Factory nextCasting = casting.tick(); 69 | if (nextCasting != null) { 70 | setCasting(nextCasting); 71 | } 72 | } 73 | 74 | public void handleEvent(final ResourceLocation id, final FriendlyByteBuf buf) { 75 | final ServerCasting casting = this.casting; 76 | if (casting != null) { 77 | casting.handleEvent(id, buf); 78 | } 79 | } 80 | 81 | public void onStartTracking(final ServerPlayer player) { 82 | final ServerCasting casting = this.casting; 83 | if (casting != null) { 84 | final FriendlyByteBuf packet = SetCastingS2CPacket.create(this.player, casting.createClientCasting()); 85 | SetCastingS2CPacket.sendTo(player, packet); 86 | } 87 | } 88 | 89 | public void onStopTracking(final ServerPlayer player) { 90 | } 91 | 92 | @Override 93 | public void close() { 94 | setCasting(null); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/ServerCastingTracker.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting; 2 | 3 | import dev.gegy.magic.casting.drawing.ServerCastingDrawing; 4 | import dev.gegy.magic.event.LateTrackingEvent; 5 | import it.unimi.dsi.fastutil.objects.Object2ObjectMap; 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 7 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 8 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; 9 | import net.fabricmc.fabric.api.networking.v1.EntityTrackingEvents; 10 | import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 11 | import net.minecraft.network.FriendlyByteBuf; 12 | import net.minecraft.resources.ResourceLocation; 13 | import net.minecraft.server.MinecraftServer; 14 | import net.minecraft.server.level.ServerPlayer; 15 | import net.minecraft.world.entity.Entity; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.util.UUID; 19 | 20 | public final class ServerCastingTracker { 21 | public static final ServerCastingTracker INSTANCE = new ServerCastingTracker(); 22 | 23 | private final Object2ObjectMap sources = new Object2ObjectOpenHashMap<>(); 24 | 25 | private ServerCastingTracker() { 26 | } 27 | 28 | public static void register() { 29 | ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> INSTANCE.onPlayerJoin(handler.player)); 30 | ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> INSTANCE.onPlayerLeave(handler.player)); 31 | 32 | ServerTickEvents.END_SERVER_TICK.register(INSTANCE::onServerTick); 33 | ServerLifecycleEvents.SERVER_STOPPING.register(INSTANCE::onServerStop); 34 | 35 | LateTrackingEvent.START.register(INSTANCE::onPlayerStartTracking); 36 | EntityTrackingEvents.STOP_TRACKING.register(INSTANCE::onPlayerStopTracking); 37 | } 38 | 39 | private void onPlayerJoin(final ServerPlayer player) { 40 | final ServerCastingSource source = new ServerCastingSource(player); 41 | source.setCasting(ServerCastingDrawing::build); 42 | 43 | sources.put(player.getUUID(), source); 44 | } 45 | 46 | private void onPlayerLeave(final ServerPlayer player) { 47 | final ServerCastingSource source = sources.remove(player.getUUID()); 48 | if (source != null) { 49 | source.close(); 50 | } 51 | } 52 | 53 | public void handleEvent(final ServerPlayer player, final ResourceLocation id, final FriendlyByteBuf buf) { 54 | final ServerCastingSource source = getSource(player); 55 | if (source != null) { 56 | source.handleEvent(id, buf); 57 | } 58 | } 59 | 60 | @Nullable 61 | private ServerCastingSource getSource(final Entity entity) { 62 | return entity instanceof ServerPlayer ? sources.get(entity.getUUID()) : null; 63 | } 64 | 65 | private void onServerStop(final MinecraftServer server) { 66 | for (final ServerCastingSource source : sources.values()) { 67 | source.close(); 68 | } 69 | sources.clear(); 70 | } 71 | 72 | private void onServerTick(final MinecraftServer server) { 73 | for (final ServerCastingSource source : sources.values()) { 74 | source.tick(); 75 | } 76 | } 77 | 78 | private void onPlayerStartTracking(final Entity trackedEntity, final ServerPlayer player) { 79 | final ServerCastingSource source = getSource(trackedEntity); 80 | if (source != null) { 81 | source.onStartTracking(player); 82 | } 83 | } 84 | 85 | private void onPlayerStopTracking(final Entity trackedEntity, final ServerPlayer player) { 86 | final ServerCastingSource source = getSource(trackedEntity); 87 | if (source != null) { 88 | source.onStopTracking(player); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/DrawingGlyphParameters.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing; 2 | 3 | import dev.gegy.magic.glyph.GlyphType; 4 | import dev.gegy.magic.glyph.shape.GlyphShape; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | import net.minecraft.network.FriendlyByteBuf; 7 | import org.jetbrains.annotations.Nullable; 8 | import org.joml.Vector3f; 9 | 10 | public record DrawingGlyphParameters( 11 | Vector3f direction, 12 | float radius, 13 | GlyphShape shape, 14 | @Nullable GlyphType formedGlyphType 15 | ) { 16 | public static final PacketCodec CODEC = PacketCodec.of(DrawingGlyphParameters::encode, DrawingGlyphParameters::decode); 17 | 18 | public boolean isFormed() { 19 | return formedGlyphType != null; 20 | } 21 | 22 | private void encode(final FriendlyByteBuf buf) { 23 | PacketCodec.VEC3F.encode(direction, buf); 24 | buf.writeFloat(radius); 25 | GlyphShape.PACKET_CODEC.encode(shape, buf); 26 | GlyphType.PACKET_CODEC.encode(formedGlyphType, buf); 27 | } 28 | 29 | private static DrawingGlyphParameters decode(final FriendlyByteBuf buf) { 30 | final Vector3f direction = PacketCodec.VEC3F.decode(buf); 31 | final float radius = buf.readFloat(); 32 | final GlyphShape shape = GlyphShape.PACKET_CODEC.decode(buf); 33 | final GlyphType formedGlyphType = GlyphType.PACKET_CODEC.decode(buf); 34 | return new DrawingGlyphParameters(direction, radius, shape, formedGlyphType); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/DrawingParameters.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing; 2 | 3 | import dev.gegy.magic.network.codec.PacketCodec; 4 | 5 | import java.util.List; 6 | 7 | public record DrawingParameters(List glyphs) { 8 | public static final PacketCodec CODEC = DrawingGlyphParameters.CODEC.list().map(DrawingParameters::new, DrawingParameters::glyphs); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/ServerDrawingGlyph.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing; 2 | 3 | import dev.gegy.magic.glyph.GlyphForm; 4 | import dev.gegy.magic.glyph.GlyphType; 5 | import dev.gegy.magic.glyph.shape.GlyphNode; 6 | import dev.gegy.magic.glyph.shape.GlyphShape; 7 | import dev.gegy.magic.glyph.shape.GlyphShapeStorage; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.joml.Vector3f; 10 | 11 | public final class ServerDrawingGlyph { 12 | private final Vector3f direction; 13 | private final float radius; 14 | 15 | private GlyphShape shape; 16 | private GlyphType formedType; 17 | 18 | private GlyphNode stroke; 19 | 20 | public ServerDrawingGlyph(final Vector3f direction, final float radius) { 21 | this.direction = direction; 22 | this.radius = radius; 23 | } 24 | 25 | public Vector3f direction() { 26 | return direction; 27 | } 28 | 29 | public float radius() { 30 | return radius; 31 | } 32 | 33 | public void setShape(final GlyphShape shape) { 34 | this.shape = shape; 35 | } 36 | 37 | public void setStroke(@Nullable final GlyphNode stroke) { 38 | this.stroke = stroke; 39 | } 40 | 41 | public boolean tryForm(final GlyphShapeStorage glyphShapes) { 42 | final GlyphShape shape = this.shape; 43 | final GlyphType matchingType = glyphShapes.getGlyphForShape(shape); 44 | if (matchingType != null) { 45 | formedType = matchingType; 46 | stroke = null; 47 | return true; 48 | } else { 49 | return false; 50 | } 51 | } 52 | 53 | public DrawingGlyphParameters asParameters() { 54 | return new DrawingGlyphParameters(direction, radius, shape, formedType); 55 | } 56 | 57 | @Nullable 58 | public GlyphForm asForm() { 59 | final GlyphType formedType = this.formedType; 60 | if (formedType != null) { 61 | return new GlyphForm(radius, shape, formedType.style()); 62 | } else { 63 | return null; 64 | } 65 | } 66 | 67 | public GlyphShape getShape() { 68 | return shape; 69 | } 70 | 71 | @Nullable 72 | public GlyphType getFormedType() { 73 | return formedType; 74 | } 75 | 76 | @Nullable 77 | public GlyphNode getStroke() { 78 | return stroke; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/ClientDrawingEventSenders.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event; 2 | 3 | import dev.gegy.magic.casting.drawing.event.c2s.BeginGlyphC2SEvent; 4 | import dev.gegy.magic.casting.drawing.event.c2s.CancelGlyphC2SEvent; 5 | import dev.gegy.magic.casting.drawing.event.c2s.DrawGlyphShapeC2SEvent; 6 | import dev.gegy.magic.casting.drawing.event.c2s.DrawGlyphStrokeC2SEvent; 7 | import dev.gegy.magic.casting.drawing.event.c2s.PrepareSpellC2SEvent; 8 | import dev.gegy.magic.client.casting.ClientCastingBuilder; 9 | import dev.gegy.magic.glyph.shape.GlyphNode; 10 | import dev.gegy.magic.glyph.shape.GlyphShape; 11 | import dev.gegy.magic.network.NetworkSender; 12 | import org.joml.Vector3f; 13 | 14 | public final class ClientDrawingEventSenders { 15 | private final NetworkSender beginGlyph; 16 | private final NetworkSender cancelGlyph; 17 | private final NetworkSender drawGlyphShape; 18 | private final NetworkSender drawGlyphStroke; 19 | private final NetworkSender prepareSpell; 20 | 21 | private ClientDrawingEventSenders( 22 | final NetworkSender beginGlyph, 23 | final NetworkSender cancelGlyph, 24 | final NetworkSender drawGlyphShape, 25 | final NetworkSender drawGlyphStroke, 26 | final NetworkSender prepareSpell 27 | ) { 28 | this.beginGlyph = beginGlyph; 29 | this.cancelGlyph = cancelGlyph; 30 | this.drawGlyphShape = drawGlyphShape; 31 | this.drawGlyphStroke = drawGlyphStroke; 32 | this.prepareSpell = prepareSpell; 33 | } 34 | 35 | public static ClientDrawingEventSenders registerTo(final ClientCastingBuilder casting) { 36 | return new ClientDrawingEventSenders( 37 | casting.registerOutboundEvent(BeginGlyphC2SEvent.SPEC), 38 | casting.registerOutboundEvent(CancelGlyphC2SEvent.SPEC), 39 | casting.registerOutboundEvent(DrawGlyphShapeC2SEvent.SPEC), 40 | casting.registerOutboundEvent(DrawGlyphStrokeC2SEvent.SPEC), 41 | casting.registerOutboundEvent(PrepareSpellC2SEvent.SPEC) 42 | ); 43 | } 44 | 45 | public void beginGlyph(final Vector3f direction, final float radius) { 46 | beginGlyph.send(new BeginGlyphC2SEvent(direction, radius)); 47 | } 48 | 49 | public void cancelGlyph() { 50 | cancelGlyph.send(new CancelGlyphC2SEvent()); 51 | } 52 | 53 | public void drawGlyphShape(final GlyphShape shape) { 54 | drawGlyphShape.send(new DrawGlyphShapeC2SEvent(shape)); 55 | } 56 | 57 | public void startGlyphStroke(final GlyphNode node) { 58 | drawGlyphStroke.send(DrawGlyphStrokeC2SEvent.start(node)); 59 | } 60 | 61 | public void stopGlyphStroke() { 62 | drawGlyphStroke.send(DrawGlyphStrokeC2SEvent.stop()); 63 | } 64 | 65 | public void prepareSpell() { 66 | prepareSpell.send(new PrepareSpellC2SEvent()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/ServerDrawingEventSenders.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event; 2 | 3 | import dev.gegy.magic.casting.ServerCastingBuilder; 4 | import dev.gegy.magic.casting.drawing.ServerDrawingGlyph; 5 | import dev.gegy.magic.casting.drawing.event.s2c.CancelDrawingS2CEvent; 6 | import dev.gegy.magic.casting.drawing.event.s2c.DrawGlyphS2CEvent; 7 | import dev.gegy.magic.casting.drawing.event.s2c.UpdateDrawingS2CEvent; 8 | import dev.gegy.magic.network.NetworkSender; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public final class ServerDrawingEventSenders { 12 | private final NetworkSender drawGlyph; 13 | private final NetworkSender updateDrawing; 14 | private final NetworkSender cancelDrawing; 15 | 16 | private ServerDrawingEventSenders( 17 | final NetworkSender drawGlyph, 18 | final NetworkSender updateDrawing, 19 | final NetworkSender cancelDrawing 20 | ) { 21 | this.drawGlyph = drawGlyph; 22 | this.updateDrawing = updateDrawing; 23 | this.cancelDrawing = cancelDrawing; 24 | } 25 | 26 | public static ServerDrawingEventSenders registerTo(final ServerCastingBuilder casting) { 27 | return new ServerDrawingEventSenders( 28 | casting.registerOutboundEvent(DrawGlyphS2CEvent.SPEC), 29 | casting.registerOutboundEvent(UpdateDrawingS2CEvent.SPEC), 30 | casting.registerOutboundEvent(CancelDrawingS2CEvent.SPEC) 31 | ); 32 | } 33 | 34 | public void drawGlyph(final ServerDrawingGlyph glyph) { 35 | drawGlyph.broadcast(new DrawGlyphS2CEvent(glyph.asParameters())); 36 | } 37 | 38 | public void cancelDrawing() { 39 | cancelDrawing.broadcast(new CancelDrawingS2CEvent()); 40 | } 41 | 42 | public void broadcastUpdateDrawing(final ServerDrawingGlyph glyph) { 43 | updateDrawing.broadcast(makeUpdateDrawing(glyph)); 44 | } 45 | 46 | public void sendUpdateDrawing(final ServerDrawingGlyph glyph) { 47 | updateDrawing.send(makeUpdateDrawing(glyph)); 48 | } 49 | 50 | @NotNull 51 | private UpdateDrawingS2CEvent makeUpdateDrawing(final ServerDrawingGlyph glyph) { 52 | return new UpdateDrawingS2CEvent(glyph.getShape(), glyph.getStroke(), glyph.getFormedType()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/c2s/BeginGlyphC2SEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.c2s; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | import net.minecraft.network.FriendlyByteBuf; 7 | import org.joml.Vector3f; 8 | 9 | public record BeginGlyphC2SEvent(Vector3f direction, float radius) { 10 | public static final PacketCodec CODEC = PacketCodec.of(BeginGlyphC2SEvent::encode, BeginGlyphC2SEvent::decode); 11 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("begin_glyph"), CODEC); 12 | 13 | private void encode(final FriendlyByteBuf buf) { 14 | PacketCodec.VEC3F.encode(direction, buf); 15 | buf.writeFloat(radius); 16 | } 17 | 18 | private static BeginGlyphC2SEvent decode(final FriendlyByteBuf buf) { 19 | final Vector3f direction = PacketCodec.VEC3F.decode(buf); 20 | final float radius = buf.readFloat(); 21 | return new BeginGlyphC2SEvent(direction, radius); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/c2s/CancelGlyphC2SEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.c2s; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | 7 | public final class CancelGlyphC2SEvent { 8 | public static final PacketCodec CODEC = PacketCodec.unit(CancelGlyphC2SEvent::new); 9 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("cancel_glyph"), CODEC); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/c2s/DrawGlyphShapeC2SEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.c2s; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.glyph.shape.GlyphShape; 6 | import dev.gegy.magic.network.codec.PacketCodec; 7 | 8 | public record DrawGlyphShapeC2SEvent(GlyphShape shape) { 9 | public static final PacketCodec CODEC = GlyphShape.PACKET_CODEC.map(DrawGlyphShapeC2SEvent::new, DrawGlyphShapeC2SEvent::shape); 10 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("glyph_shape"), CODEC); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/c2s/DrawGlyphStrokeC2SEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.c2s; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.glyph.shape.GlyphNode; 6 | import dev.gegy.magic.network.codec.PacketCodec; 7 | import net.minecraft.network.FriendlyByteBuf; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | public record DrawGlyphStrokeC2SEvent(@Nullable GlyphNode node) { 12 | public static final PacketCodec CODEC = PacketCodec.of(DrawGlyphStrokeC2SEvent::encode, DrawGlyphStrokeC2SEvent::decode); 13 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("glyph_stroke"), CODEC); 14 | 15 | private static final DrawGlyphStrokeC2SEvent STOP = new DrawGlyphStrokeC2SEvent(null); 16 | 17 | public static DrawGlyphStrokeC2SEvent start(@NotNull final GlyphNode node) { 18 | return new DrawGlyphStrokeC2SEvent(node); 19 | } 20 | 21 | public static DrawGlyphStrokeC2SEvent stop() { 22 | return STOP; 23 | } 24 | 25 | private void encode(final FriendlyByteBuf buf) { 26 | GlyphNode.PACKET_CODEC.encode(node, buf); 27 | } 28 | 29 | private static DrawGlyphStrokeC2SEvent decode(final FriendlyByteBuf buf) { 30 | final GlyphNode node = GlyphNode.PACKET_CODEC.decode(buf); 31 | return new DrawGlyphStrokeC2SEvent(node); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/c2s/PrepareSpellC2SEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.c2s; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | 7 | public final class PrepareSpellC2SEvent { 8 | public static final PacketCodec CODEC = PacketCodec.unit(PrepareSpellC2SEvent::new); 9 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("prepare_spell"), CODEC); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/s2c/CancelDrawingS2CEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.s2c; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | 7 | public record CancelDrawingS2CEvent() { 8 | public static final PacketCodec CODEC = PacketCodec.unit(CancelDrawingS2CEvent::new); 9 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("cancel_drawing"), CODEC); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/s2c/DrawGlyphS2CEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.s2c; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.drawing.DrawingGlyphParameters; 5 | import dev.gegy.magic.casting.event.CastingEventSpec; 6 | import dev.gegy.magic.network.codec.PacketCodec; 7 | 8 | public record DrawGlyphS2CEvent(DrawingGlyphParameters glyph) { 9 | public static final PacketCodec CODEC = DrawingGlyphParameters.CODEC.map(DrawGlyphS2CEvent::new, DrawGlyphS2CEvent::glyph); 10 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("draw_glyph"), CODEC); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/drawing/event/s2c/UpdateDrawingS2CEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.drawing.event.s2c; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.glyph.GlyphType; 6 | import dev.gegy.magic.glyph.shape.GlyphNode; 7 | import dev.gegy.magic.glyph.shape.GlyphShape; 8 | import dev.gegy.magic.network.codec.PacketCodec; 9 | import net.minecraft.network.FriendlyByteBuf; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | public record UpdateDrawingS2CEvent( 13 | GlyphShape shape, 14 | @Nullable GlyphNode stroke, 15 | @Nullable GlyphType formedGlyphType 16 | ) { 17 | public static final PacketCodec CODEC = PacketCodec.of(UpdateDrawingS2CEvent::encode, UpdateDrawingS2CEvent::decode); 18 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("update_drawing"), CODEC); 19 | 20 | private void encode(final FriendlyByteBuf buf) { 21 | GlyphShape.PACKET_CODEC.encode(shape, buf); 22 | GlyphNode.PACKET_CODEC.encode(stroke, buf); 23 | GlyphType.PACKET_CODEC.encode(formedGlyphType, buf); 24 | } 25 | 26 | private static UpdateDrawingS2CEvent decode(final FriendlyByteBuf buf) { 27 | final GlyphShape shape = GlyphShape.PACKET_CODEC.decode(buf); 28 | final GlyphNode stroke = GlyphNode.PACKET_CODEC.decode(buf); 29 | final GlyphType formedGlyphType = GlyphType.PACKET_CODEC.decode(buf); 30 | return new UpdateDrawingS2CEvent(shape, stroke, formedGlyphType); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/event/CastingEventSpec.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.event; 2 | 3 | import dev.gegy.magic.network.codec.PacketCodec; 4 | import net.minecraft.resources.ResourceLocation; 5 | 6 | public record CastingEventSpec( 7 | ResourceLocation id, 8 | PacketCodec codec 9 | ) { 10 | public static CastingEventSpec of(final ResourceLocation id, final PacketCodec codec) { 11 | return new CastingEventSpec<>(id, codec); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/event/EventSenderFactory.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.event; 2 | 3 | import dev.gegy.magic.network.NetworkSender; 4 | 5 | public interface EventSenderFactory { 6 | NetworkSender create(CastingEventSpec spec); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/event/InboundCastingEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.event; 2 | 3 | import dev.gegy.magic.network.codec.PacketDecoder; 4 | import net.minecraft.network.FriendlyByteBuf; 5 | import net.minecraft.resources.ResourceLocation; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | 11 | public final class InboundCastingEvent { 12 | private final ResourceLocation id; 13 | private final PacketDecoder decoder; 14 | private final List> handlers = new ArrayList<>(); 15 | 16 | public InboundCastingEvent(final ResourceLocation id, final PacketDecoder decoder) { 17 | this.id = id; 18 | this.decoder = decoder; 19 | } 20 | 21 | public InboundCastingEvent addHandler(final Consumer handler) { 22 | handlers.add(handler); 23 | return this; 24 | } 25 | 26 | public ResourceLocation id() { 27 | return id; 28 | } 29 | 30 | public T decode(final FriendlyByteBuf buf) { 31 | return decoder.decode(buf); 32 | } 33 | 34 | public void accept(final T event) { 35 | for (final Consumer handler : handlers) { 36 | handler.accept(event); 37 | } 38 | } 39 | 40 | public void acceptBytes(final FriendlyByteBuf buf) { 41 | final T event = decoder.decode(buf); 42 | accept(event); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/SpellParameters.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell; 2 | 3 | import dev.gegy.magic.glyph.GlyphForm; 4 | import dev.gegy.magic.network.codec.PacketCodec; 5 | import net.minecraft.network.FriendlyByteBuf; 6 | import org.joml.Vector3f; 7 | 8 | import java.util.List; 9 | 10 | public record SpellParameters( 11 | List glyphs, 12 | Vector3f direction 13 | ) { 14 | public static final PacketCodec CODEC = PacketCodec.of(SpellParameters::encode, SpellParameters::decode); 15 | 16 | private void encode(final FriendlyByteBuf buf) { 17 | GlyphForm.PACKET_CODEC.list().encode(glyphs, buf); 18 | PacketCodec.VEC3F.encode(direction, buf); 19 | } 20 | 21 | private static SpellParameters decode(final FriendlyByteBuf buf) { 22 | final List glyphs = GlyphForm.PACKET_CODEC.list().decode(buf); 23 | final Vector3f direction = PacketCodec.VEC3F.decode(buf); 24 | return new SpellParameters(glyphs, direction); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/beam/BeamParameters.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.beam; 2 | 3 | import dev.gegy.magic.casting.spell.SpellParameters; 4 | import dev.gegy.magic.network.codec.PacketCodec; 5 | import net.minecraft.network.FriendlyByteBuf; 6 | 7 | public record BeamParameters( 8 | SpellParameters spell, 9 | boolean active 10 | ) { 11 | public static final PacketCodec CODEC = PacketCodec.of(BeamParameters::encode, BeamParameters::decode); 12 | 13 | private void encode(final FriendlyByteBuf buf) { 14 | SpellParameters.CODEC.encode(spell, buf); 15 | buf.writeBoolean(active); 16 | } 17 | 18 | private static BeamParameters decode(final FriendlyByteBuf buf) { 19 | final SpellParameters spell = SpellParameters.CODEC.decode(buf); 20 | final boolean active = buf.readBoolean(); 21 | return new BeamParameters(spell, active); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/beam/ServerCastingBeam.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.beam; 2 | 3 | import dev.gegy.magic.casting.ServerCasting; 4 | import dev.gegy.magic.casting.ServerCastingBuilder; 5 | import dev.gegy.magic.casting.drawing.ServerCastingDrawing; 6 | import dev.gegy.magic.casting.spell.SpellParameters; 7 | import dev.gegy.magic.client.casting.ClientCastingType; 8 | import dev.gegy.magic.network.NetworkSender; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | public final class ServerCastingBeam { 13 | public static final int MAXIMUM_LENGTH = 8; 14 | 15 | private final ServerPlayer player; 16 | private final SpellParameters spell; 17 | private final EventSenders eventSenders; 18 | 19 | private boolean active; 20 | 21 | private ServerCastingBeam(final ServerPlayer player, final SpellParameters spell, final EventSenders eventSenders) { 22 | this.player = player; 23 | this.spell = spell; 24 | this.eventSenders = eventSenders; 25 | } 26 | 27 | public static ServerCasting build(final ServerPlayer player, final SpellParameters spell, final ServerCastingBuilder casting) { 28 | final EventSenders eventSenders = EventSenders.register(casting); 29 | final ServerCastingBeam beam = new ServerCastingBeam(player, spell, eventSenders); 30 | 31 | casting.registerClientCasting(ClientCastingType.BEAM, beam::buildParameters); 32 | casting.registerTicker(beam::tick); 33 | 34 | casting.bindInboundEvent(SetBeamActive.SPEC, beam::handleSetActive); 35 | 36 | return casting.build(); 37 | } 38 | 39 | private void handleSetActive(final SetBeamActive event) { 40 | active = event.active(); 41 | eventSenders.setActive(event.active()); 42 | } 43 | 44 | @Nullable 45 | private ServerCasting.Factory tick() { 46 | // TODO: proper cancel logic 47 | if (player.isShiftKeyDown() && !active) { 48 | return ServerCastingDrawing::build; 49 | } 50 | return null; 51 | } 52 | 53 | private BeamParameters buildParameters() { 54 | return new BeamParameters(spell, active); 55 | } 56 | 57 | private record EventSenders(NetworkSender setActive) { 58 | public static EventSenders register(final ServerCastingBuilder casting) { 59 | return new EventSenders( 60 | casting.registerOutboundEvent(SetBeamActive.SPEC) 61 | ); 62 | } 63 | 64 | public void setActive(final boolean active) { 65 | setActive.broadcast(new SetBeamActive(active)); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/beam/SetBeamActive.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.beam; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | 7 | public record SetBeamActive(boolean active) { 8 | public static final PacketCodec CODEC = PacketCodec.BOOLEAN.map(SetBeamActive::new, SetBeamActive::active); 9 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("set_active"), CODEC); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/teleport/SelectTeleportTarget.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.teleport; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | 7 | import java.util.UUID; 8 | 9 | public record SelectTeleportTarget(UUID targetId) { 10 | public static final PacketCodec CODEC = PacketCodec.UUID 11 | .map(SelectTeleportTarget::new, SelectTeleportTarget::targetId); 12 | 13 | public static final CastingEventSpec SPEC = CastingEventSpec.of(Magic.identifier("select_target"), CODEC); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/teleport/TeleportInput.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.teleport; 2 | 3 | import dev.gegy.magic.client.casting.spell.teleport.ClientCastingTeleport; 4 | import net.minecraft.world.entity.player.Player; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | public final class TeleportInput { 8 | @Nullable 9 | public TeleportTarget tick(final ClientCastingTeleport casting, final Player player) { 10 | // TODO: implement teleportation input 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/teleport/TeleportParameters.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.teleport; 2 | 3 | import dev.gegy.magic.casting.spell.SpellParameters; 4 | import dev.gegy.magic.network.codec.PacketCodec; 5 | import net.minecraft.network.FriendlyByteBuf; 6 | 7 | import java.util.Map; 8 | import java.util.UUID; 9 | 10 | public record TeleportParameters( 11 | SpellParameters spell, 12 | Map targets 13 | ) { 14 | public static final PacketCodec CODEC = PacketCodec.of(TeleportParameters::encode, TeleportParameters::decode); 15 | 16 | private static final PacketCodec> SYMBOL_MAP_CODEC = PacketCodec.mapOf(PacketCodec.UUID, TeleportTargetSymbol.CODEC); 17 | 18 | private void encode(final FriendlyByteBuf buf) { 19 | SpellParameters.CODEC.encode(spell, buf); 20 | SYMBOL_MAP_CODEC.encode(targets, buf); 21 | } 22 | 23 | private static TeleportParameters decode(final FriendlyByteBuf buf) { 24 | final SpellParameters spell = SpellParameters.CODEC.decode(buf); 25 | final Map symbols = SYMBOL_MAP_CODEC.decode(buf); 26 | return new TeleportParameters(spell, symbols); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/teleport/TeleportTarget.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.teleport; 2 | 3 | import net.minecraft.core.BlockPos; 4 | import net.minecraft.resources.ResourceKey; 5 | import net.minecraft.world.level.Level; 6 | 7 | import java.util.UUID; 8 | 9 | public record TeleportTarget( 10 | UUID id, TeleportTargetSymbol symbol, 11 | ResourceKey dimension, BlockPos pos, float angle 12 | ) { 13 | public static TeleportTarget create(final TeleportTargetSymbol symbol, final ResourceKey dimension, final BlockPos pos, final float angle) { 14 | return new TeleportTarget(UUID.randomUUID(), symbol, dimension, pos, angle); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/casting/spell/teleport/TeleportTargetSymbol.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.casting.spell.teleport; 2 | 3 | import dev.gegy.magic.math.ColorRgb; 4 | import dev.gegy.magic.network.codec.PacketCodec; 5 | import net.minecraft.network.FriendlyByteBuf; 6 | 7 | public record TeleportTargetSymbol( 8 | char character, 9 | ColorRgb color 10 | ) { 11 | public static final PacketCodec CODEC = PacketCodec.of(TeleportTargetSymbol::encode, TeleportTargetSymbol::decode); 12 | 13 | private void encode(final FriendlyByteBuf buf) { 14 | buf.writeChar(character); 15 | ColorRgb.PACKET_CODEC.encode(color, buf); 16 | } 17 | 18 | private static TeleportTargetSymbol decode(final FriendlyByteBuf buf) { 19 | final char character = buf.readChar(); 20 | final ColorRgb color = ColorRgb.PACKET_CODEC.decode(buf); 21 | return new TeleportTargetSymbol(character, color); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/animator/ArmPose.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.animator; 2 | 3 | import dev.gegy.magic.client.glyph.GlyphPlane; 4 | import net.minecraft.client.model.geom.ModelPart; 5 | import net.minecraft.util.Mth; 6 | import net.minecraft.world.entity.LivingEntity; 7 | import org.joml.Vector3f; 8 | 9 | public final class ArmPose { 10 | private static final double HALF_PI = Math.PI / 2.0; 11 | 12 | private final Vector3f target = new Vector3f(); 13 | private final Vector3f prevTarget = new Vector3f(); 14 | 15 | public void resetInterpolation() { 16 | prevTarget.set(target); 17 | } 18 | 19 | public void pointTo(final LivingEntity entity, final Vector3f newTarget) { 20 | prevTarget.set(target); 21 | 22 | final Vector3f target = this.target.set(newTarget) 23 | .rotateY(entity.yBodyRot * Mth.DEG_TO_RAD) 24 | .add(0.0f, entity.getEyeHeight(), 0.0f); 25 | 26 | target.set( 27 | target.x() * 16.0f, 28 | 24.0f - target.y() * 16.0f, 29 | target.z() * 16.0f 30 | ); 31 | } 32 | 33 | public void pointToPointOnPlane(final LivingEntity entity, final GlyphPlane plane, final Vector3f target) { 34 | pointTo(entity, plane.projectToWorld(target)); 35 | } 36 | 37 | public void apply(final ModelPart part, final float tickDelta, final float weight) { 38 | final Vector3f prevTarget = this.prevTarget; 39 | final Vector3f target = this.target; 40 | 41 | final float targetX = Mth.lerp(tickDelta, prevTarget.x(), target.x()); 42 | final float targetY = Mth.lerp(tickDelta, prevTarget.y(), target.y()); 43 | final float targetZ = Mth.lerp(tickDelta, prevTarget.z(), target.z()); 44 | 45 | final float deltaX = targetX - part.x; 46 | final float deltaY = targetY - part.y; 47 | final float deltaZ = targetZ - part.z; 48 | final double deltaXZ = Mth.length(deltaX, deltaZ); 49 | 50 | final float targetYRot = (float) -Math.atan2(deltaX, deltaZ); 51 | final float targetXRot = (float) (Math.atan2(deltaY, deltaXZ) - HALF_PI); 52 | 53 | part.yRot = Mth.lerp(part.yRot, targetYRot, weight); 54 | part.xRot = Mth.lerp(part.xRot, targetXRot, weight); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/animator/CastingAnimatableEntity.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.animator; 2 | 3 | public interface CastingAnimatableEntity { 4 | CastingAnimator getCastingAnimator(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/animator/CastingAnimator.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.animator; 2 | 3 | import net.minecraft.client.model.geom.ModelPart; 4 | import net.minecraft.world.entity.player.Player; 5 | 6 | public final class CastingAnimator { 7 | private static final int POSE_BLEND_TICKS = 5; 8 | 9 | private final CastingPose prepared = new CastingPose.Prepared(); 10 | private final CastingPose drawing = new CastingPose.Drawing(); 11 | 12 | private CastingPose pose; 13 | private CastingPose prevPose; 14 | private int poseBlendingTicks; 15 | 16 | public void applyToModel(final Player entity, final ModelPart leftArm, final ModelPart rightArm, final float tickDelta) { 17 | final CastingPose pose = this.pose; 18 | final CastingPose prevPose = this.prevPose; 19 | if (pose == null && prevPose == null) { 20 | return; 21 | } 22 | 23 | if (pose == prevPose) { 24 | applyToModelStable(entity, pose, leftArm, rightArm, tickDelta); 25 | } else { 26 | final float blendTicks = poseBlendingTicks + tickDelta; 27 | applyToModelBlended(entity, prevPose, pose, blendTicks, leftArm, rightArm, tickDelta); 28 | } 29 | } 30 | 31 | private void applyToModelStable(final Player entity, final CastingPose pose, final ModelPart leftArm, final ModelPart rightArm, final float tickDelta) { 32 | pose.apply(entity, leftArm, rightArm, tickDelta, 1.0f); 33 | } 34 | 35 | private void applyToModelBlended( 36 | final Player entity, 37 | final CastingPose prevPose, final CastingPose pose, final float transitionTicks, 38 | final ModelPart leftArm, final ModelPart rightArm, final float tickDelta 39 | ) { 40 | final float weight = transitionTicks / POSE_BLEND_TICKS; 41 | final float prevWeight = 1.0f - weight; 42 | 43 | if (prevPose != null) { 44 | prevPose.apply(entity, leftArm, rightArm, tickDelta, prevWeight); 45 | } 46 | 47 | if (pose != null) { 48 | pose.apply(entity, leftArm, rightArm, tickDelta, weight); 49 | } 50 | } 51 | 52 | public void tick(final Player entity) { 53 | if (pose == prevPose) { 54 | tickStable(entity); 55 | } else { 56 | tickBlending(entity); 57 | } 58 | } 59 | 60 | private void tickStable(final Player entity) { 61 | if (tickPose(entity, drawing)) return; 62 | if (tickPose(entity, prepared)) return; 63 | 64 | pose = null; 65 | } 66 | 67 | private boolean tickPose(final Player entity, final CastingPose pose) { 68 | if (pose.tick(entity)) { 69 | if (this.pose != pose) { 70 | this.pose = pose; 71 | pose.beginAnimating(); 72 | } 73 | return true; 74 | } else { 75 | return false; 76 | } 77 | } 78 | 79 | private void tickBlending(final Player entity) { 80 | if (++poseBlendingTicks >= POSE_BLEND_TICKS) { 81 | poseBlendingTicks = 0; 82 | prevPose = pose; 83 | } 84 | 85 | // just tick our current pose: we don't want to change poses while blending 86 | if (pose != null) { 87 | pose.tick(entity); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/animator/CastingPose.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.animator; 2 | 3 | import dev.gegy.magic.client.casting.ClientCastingTracker; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.client.effect.EffectSelector; 6 | import dev.gegy.magic.client.effect.casting.drawing.DrawingEffect; 7 | import dev.gegy.magic.client.effect.casting.spell.PreparedSpellEffect; 8 | import dev.gegy.magic.client.glyph.spell.transform.SpellTransform; 9 | import net.minecraft.client.model.geom.ModelPart; 10 | import net.minecraft.world.entity.HumanoidArm; 11 | import net.minecraft.world.entity.player.Player; 12 | import org.joml.Vector3f; 13 | 14 | public interface CastingPose { 15 | void beginAnimating(); 16 | 17 | boolean tick(Player entity); 18 | 19 | void apply(Player entity, ModelPart leftArm, ModelPart rightArm, float tickDelta, float weight); 20 | 21 | final class Drawing implements CastingPose { 22 | private final ArmPose leftArm = new ArmPose(); 23 | private final ArmPose rightArm = new ArmPose(); 24 | 25 | private final Vector3f target = new Vector3f(); 26 | 27 | @Override 28 | public void beginAnimating() { 29 | leftArm.resetInterpolation(); 30 | rightArm.resetInterpolation(); 31 | } 32 | 33 | @Override 34 | public boolean tick(final Player entity) { 35 | final EffectSelector effects = ClientCastingTracker.INSTANCE.effectSelectorFor(entity); 36 | 37 | final DrawingEffect drawing = effects.selectAny(DrawingEffect.TYPE); 38 | if (drawing == null || drawing.getGlyph() == null) { 39 | return false; 40 | } 41 | 42 | final ClientDrawingGlyph glyph = drawing.getGlyph(); 43 | final Vector3f pointer = glyph.drawPointer(); 44 | if (pointer == null) { 45 | return false; 46 | } 47 | 48 | final float leftX = Math.abs(pointer.x()); 49 | final float rightX = -leftX; 50 | 51 | final Vector3f target = this.target.set(leftX, pointer.y(), pointer.z()); 52 | leftArm.pointToPointOnPlane(entity, glyph.plane(), target); 53 | 54 | target.set(rightX, pointer.y(), pointer.z()); 55 | rightArm.pointToPointOnPlane(entity, glyph.plane(), target); 56 | 57 | return true; 58 | } 59 | 60 | @Override 61 | public void apply(final Player entity, final ModelPart leftArm, final ModelPart rightArm, final float tickDelta, final float weight) { 62 | this.leftArm.apply(leftArm, tickDelta, weight); 63 | this.rightArm.apply(rightArm, tickDelta, weight); 64 | } 65 | } 66 | 67 | final class Prepared implements CastingPose { 68 | private final ArmPose mainArm = new ArmPose(); 69 | 70 | @Override 71 | public void beginAnimating() { 72 | mainArm.resetInterpolation(); 73 | } 74 | 75 | @Override 76 | public boolean tick(final Player entity) { 77 | final EffectSelector effects = ClientCastingTracker.INSTANCE.effectSelectorFor(entity); 78 | final PreparedSpellEffect preparedSpell = effects.selectAny(PreparedSpellEffect.TYPE); 79 | if (preparedSpell == null) { 80 | return false; 81 | } 82 | 83 | final SpellTransform transform = preparedSpell.spell().transform(); 84 | 85 | final Vector3f target = transform.getOrigin(1.0f); 86 | mainArm.pointTo(entity, target); 87 | 88 | return true; 89 | } 90 | 91 | @Override 92 | public void apply(final Player entity, final ModelPart leftArm, final ModelPart rightArm, final float tickDelta, final float weight) { 93 | final ModelPart armPart = entity.getMainArm() == HumanoidArm.LEFT ? leftArm : rightArm; 94 | mainArm.apply(armPart, tickDelta, weight); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/ClientCasting.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.client.casting.blend.CastingBlender; 5 | import dev.gegy.magic.client.effect.EffectSelector; 6 | import net.minecraft.network.FriendlyByteBuf; 7 | import net.minecraft.resources.ResourceLocation; 8 | import net.minecraft.world.entity.player.Player; 9 | 10 | public interface ClientCasting { 11 | ClientCasting NONE = new ClientCasting() { 12 | @Override 13 | public ClientCasting tick() { 14 | return this; 15 | } 16 | 17 | @Override 18 | public void handleEvent(final ResourceLocation id, final FriendlyByteBuf buf) { 19 | Magic.LOGGER.warn("Received unexpected inbound casting event '{}' while no casting is active", id); 20 | } 21 | 22 | @Override 23 | public EffectSelector getEffects() { 24 | return EffectSelector.EMPTY; 25 | } 26 | }; 27 | 28 | default ClientCasting handleServerCast(final ClientCasting casting) { 29 | return casting; 30 | } 31 | 32 | ClientCasting tick(); 33 | 34 | void handleEvent(ResourceLocation id, FriendlyByteBuf buf); 35 | 36 | EffectSelector getEffects(); 37 | 38 | default CastingBlender getBlender() { 39 | return CastingBlender.EMPTY; 40 | } 41 | 42 | interface Factory

{ 43 | ClientCasting build(Player player, P parameters, ClientCastingBuilder casting); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/ClientCastingSource.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting; 2 | 3 | import dev.gegy.magic.casting.event.CastingEventSpec; 4 | import dev.gegy.magic.client.casting.blend.CastingBlender; 5 | import dev.gegy.magic.client.effect.Effect; 6 | import dev.gegy.magic.client.effect.EffectManager; 7 | import dev.gegy.magic.client.effect.EffectSelector; 8 | import dev.gegy.magic.network.NetworkAddressing; 9 | import dev.gegy.magic.network.NetworkSender; 10 | import dev.gegy.magic.network.c2s.CastingEventC2SPacket; 11 | import net.minecraft.network.FriendlyByteBuf; 12 | import net.minecraft.resources.ResourceLocation; 13 | import net.minecraft.world.entity.player.Player; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | public final class ClientCastingSource implements AutoCloseable { 18 | private final Player player; 19 | 20 | private ClientCasting casting = ClientCasting.NONE; 21 | 22 | public ClientCastingSource(final Player player) { 23 | this.player = player; 24 | } 25 | 26 | public

void handleServerCast(@Nullable final ConfiguredClientCasting

configuredCasting) { 27 | final CastingBlender blender = casting.getBlender(); 28 | 29 | final ClientCastingBuilder builder = createCastingBuilder(blender); 30 | if (configuredCasting != null) { 31 | handleServerCast(configuredCasting.build(player, builder)); 32 | } else { 33 | handleServerCast(builder.build()); 34 | } 35 | } 36 | 37 | private void handleServerCast(final ClientCasting casting) { 38 | setCasting(this.casting.handleServerCast(casting)); 39 | } 40 | 41 | @NotNull 42 | private ClientCastingBuilder createCastingBuilder(final CastingBlender blender) { 43 | return new ClientCastingBuilder(ClientCastingSource::createEventSender, blender); 44 | } 45 | 46 | private static NetworkSender createEventSender(final CastingEventSpec spec) { 47 | return NetworkAddressing.server().sender(($, event) -> CastingEventC2SPacket.sendToServer(spec, event)); 48 | } 49 | 50 | private void setCasting(final ClientCasting newCasting) { 51 | final ClientCasting oldCasting = casting; 52 | closeCasting(oldCasting); 53 | setupCasting(newCasting); 54 | 55 | casting = newCasting; 56 | } 57 | 58 | private void setupCasting(final ClientCasting casting) { 59 | final EffectManager effectManager = EffectManager.get(); 60 | for (final Effect effect : casting.getEffects()) { 61 | effectManager.add(effect); 62 | } 63 | } 64 | 65 | private void closeCasting(final ClientCasting casting) { 66 | final EffectManager effectManager = EffectManager.get(); 67 | for (final Effect effect : casting.getEffects()) { 68 | effectManager.remove(effect); 69 | } 70 | } 71 | 72 | public void tick() { 73 | final ClientCasting casting = this.casting; 74 | final ClientCasting nextCasting = casting.tick(); 75 | if (nextCasting != casting) { 76 | setCasting(nextCasting); 77 | } 78 | } 79 | 80 | public void handleEvent(final ResourceLocation id, final FriendlyByteBuf buf) { 81 | casting.handleEvent(id, buf); 82 | } 83 | 84 | public EffectSelector effectSelector() { 85 | return casting.getEffects(); 86 | } 87 | 88 | @Override 89 | public void close() { 90 | setCasting(ClientCasting.NONE); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/ClientCastingTracker.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting; 2 | 3 | import dev.gegy.magic.client.effect.EffectSelector; 4 | import dev.gegy.magic.client.event.ClientRemoveEntityEvent; 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 6 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 7 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.player.LocalPlayer; 10 | import net.minecraft.network.FriendlyByteBuf; 11 | import net.minecraft.resources.ResourceLocation; 12 | import net.minecraft.world.entity.Entity; 13 | import net.minecraft.world.entity.player.Player; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | public final class ClientCastingTracker { 18 | public static final ClientCastingTracker INSTANCE = new ClientCastingTracker(); 19 | 20 | private final Int2ObjectMap sources = new Int2ObjectOpenHashMap<>(); 21 | 22 | private ClientCastingTracker() { 23 | } 24 | 25 | public static void register() { 26 | ClientTickEvents.END_CLIENT_TICK.register(INSTANCE::tick); 27 | ClientRemoveEntityEvent.EVENT.register(INSTANCE::removeSource); 28 | } 29 | 30 | public EffectSelector effectSelectorFor(final Player player) { 31 | final ClientCastingSource source = getSource(player); 32 | return source != null ? source.effectSelector() : EffectSelector.EMPTY; 33 | } 34 | 35 | public void handleEvent(final Player entity, final ResourceLocation id, final FriendlyByteBuf buf) { 36 | final ClientCastingSource source = getOrCreateSource(entity); 37 | source.handleEvent(id, buf); 38 | } 39 | 40 | public void setCasting(final Player player, @Nullable final ConfiguredClientCasting casting) { 41 | final ClientCastingSource source = getOrCreateSource(player); 42 | source.handleServerCast(casting); 43 | } 44 | 45 | @NotNull 46 | private ClientCastingSource getOrCreateSource(final Player player) { 47 | return sources.computeIfAbsent(player.getId(), i -> new ClientCastingSource(player)); 48 | } 49 | 50 | @Nullable 51 | private ClientCastingSource getSource(final Player player) { 52 | return sources.get(player.getId()); 53 | } 54 | 55 | private void tick(final Minecraft client) { 56 | final LocalPlayer player = client.player; 57 | if (player == null) { 58 | clearSources(); 59 | return; 60 | } 61 | 62 | for (final ClientCastingSource source : sources.values()) { 63 | source.tick(); 64 | } 65 | } 66 | 67 | private void clearSources() { 68 | if (!sources.isEmpty()) { 69 | for (final ClientCastingSource source : sources.values()) { 70 | source.close(); 71 | } 72 | sources.clear(); 73 | } 74 | } 75 | 76 | private void removeSource(final Entity entity) { 77 | final ClientCastingSource source = sources.remove(entity.getId()); 78 | if (source != null) { 79 | source.close(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/ClientCastingType.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.drawing.DrawingParameters; 5 | import dev.gegy.magic.casting.spell.beam.BeamParameters; 6 | import dev.gegy.magic.casting.spell.teleport.TeleportParameters; 7 | import dev.gegy.magic.client.casting.drawing.ClientCastingDrawing; 8 | import dev.gegy.magic.client.casting.spell.beam.ClientCastingBeam; 9 | import dev.gegy.magic.client.casting.spell.teleport.ClientCastingTeleport; 10 | import dev.gegy.magic.network.codec.PacketCodec; 11 | import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder; 12 | import net.fabricmc.fabric.api.event.registry.RegistryAttribute; 13 | import net.minecraft.core.MappedRegistry; 14 | import net.minecraft.core.Registry; 15 | 16 | public final class ClientCastingType

{ 17 | public static final MappedRegistry> REGISTRY = FabricRegistryBuilder.createSimple(ClientCastingType.type(), Magic.identifier("casting")) 18 | .attribute(RegistryAttribute.SYNCED) 19 | .buildAndRegister(); 20 | 21 | public static final PacketCodec> PACKET_CODEC = PacketCodec.ofRegistry(REGISTRY); 22 | 23 | public static final ClientCastingType DRAWING = register("drawing", ClientCastingDrawing::build, DrawingParameters.CODEC); 24 | public static final ClientCastingType BEAM = register("beam", ClientCastingBeam::build, BeamParameters.CODEC); 25 | public static final ClientCastingType TELEPORT = register("teleport", ClientCastingTeleport::build, TeleportParameters.CODEC); 26 | 27 | private final ClientCasting.Factory

factory; 28 | private final PacketCodec

parametersCodec; 29 | 30 | public ClientCastingType(final ClientCasting.Factory

factory, final PacketCodec

parametersCodec) { 31 | this.factory = factory; 32 | this.parametersCodec = parametersCodec; 33 | } 34 | 35 | public static void onInitialize() { 36 | } 37 | 38 | private static

ClientCastingType

register(final String id, final ClientCasting.Factory

factory, final PacketCodec

parametersCodec) { 39 | return Registry.register(REGISTRY, Magic.identifier(id), new ClientCastingType<>(factory, parametersCodec)); 40 | } 41 | 42 | public ClientCasting.Factory

factory() { 43 | return factory; 44 | } 45 | 46 | public PacketCodec

parametersCodec() { 47 | return parametersCodec; 48 | } 49 | 50 | public ConfiguredClientCasting

configure(final P parameters) { 51 | return new ConfiguredClientCasting<>(this, parameters); 52 | } 53 | 54 | @SuppressWarnings("unchecked") 55 | private static Class> type() { 56 | return (Class>) (Object) ClientCastingType.class; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/ConfiguredClientCasting.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting; 2 | 3 | import dev.gegy.magic.network.codec.PacketCodec; 4 | import net.minecraft.network.FriendlyByteBuf; 5 | import net.minecraft.world.entity.player.Player; 6 | 7 | public record ConfiguredClientCasting

(ClientCastingType

type, P parameters) { 8 | public static final PacketCodec> CODEC = PacketCodec.of( 9 | ConfiguredClientCasting::encode, 10 | ConfiguredClientCasting::decode 11 | ); 12 | 13 | public ClientCasting build(final Player player, final ClientCastingBuilder casting) { 14 | return type.factory().build(player, parameters, casting); 15 | } 16 | 17 | private void encode(final FriendlyByteBuf buf) { 18 | ClientCastingType.PACKET_CODEC.encode(type, buf); 19 | type.parametersCodec().encode(parameters, buf); 20 | } 21 | 22 | private static ConfiguredClientCasting decode(final FriendlyByteBuf buf) { 23 | final ClientCastingType type = ClientCastingType.PACKET_CODEC.decode(buf); 24 | return decodeTyped(type, buf); 25 | } 26 | 27 | private static

ConfiguredClientCasting

decodeTyped(final ClientCastingType

type, final FriendlyByteBuf buf) { 28 | final P parameters = type.parametersCodec().decode(buf); 29 | return new ConfiguredClientCasting<>(type, parameters); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/blend/CastingBlendBuilder.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.blend; 2 | 3 | import dev.gegy.magic.client.casting.ClientCasting; 4 | import dev.gegy.magic.client.effect.Effect; 5 | import dev.gegy.magic.client.effect.EffectMap; 6 | import dev.gegy.magic.client.effect.EffectSelector; 7 | import net.minecraft.network.FriendlyByteBuf; 8 | import net.minecraft.resources.ResourceLocation; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public final class CastingBlendBuilder { 14 | private final EffectMap effects = new EffectMap(); 15 | private final List tickers = new ArrayList<>(); 16 | 17 | public E attachEffect(final E effect) { 18 | effects.add(effect); 19 | return effect; 20 | } 21 | 22 | public void registerTicker(final Ticker ticker) { 23 | tickers.add(ticker); 24 | } 25 | 26 | public void registerTicker(final Runnable ticker) { 27 | registerTicker(() -> { 28 | ticker.run(); 29 | return false; 30 | }); 31 | } 32 | 33 | public ClientCasting build(final ClientCasting target) { 34 | if (!isEmpty()) { 35 | return new BlendingCasting(target, effects, tickers); 36 | } else { 37 | return target; 38 | } 39 | } 40 | 41 | public boolean isEmpty() { 42 | return effects.isEmpty() && tickers.isEmpty(); 43 | } 44 | 45 | public interface Ticker { 46 | boolean tick(); 47 | } 48 | 49 | private static final class BlendingCasting implements ClientCasting { 50 | private ClientCasting target; 51 | private final EffectMap effects; 52 | private final List tickers; 53 | 54 | BlendingCasting(final ClientCasting target, final EffectMap effects, final List tickers) { 55 | this.target = target; 56 | this.effects = effects; 57 | this.tickers = tickers; 58 | } 59 | 60 | @Override 61 | public ClientCasting handleServerCast(final ClientCasting casting) { 62 | target = casting; 63 | return this; 64 | } 65 | 66 | @Override 67 | public ClientCasting tick() { 68 | for (final Ticker ticker : tickers) { 69 | if (ticker.tick()) { 70 | return target; 71 | } 72 | } 73 | return this; 74 | } 75 | 76 | @Override 77 | public void handleEvent(final ResourceLocation id, final FriendlyByteBuf buf) { 78 | final ClientCasting target = this.target; 79 | if (target != null) { 80 | target.handleEvent(id, buf); 81 | } 82 | } 83 | 84 | @Override 85 | public EffectSelector getEffects() { 86 | return effects; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/blend/CastingBlendType.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.blend; 2 | 3 | import dev.gegy.magic.client.glyph.spell.Spell; 4 | import dev.gegy.magic.client.glyph.spell.transform.SpellTransformType; 5 | 6 | public final class CastingBlendType { 7 | public static final CastingBlendType SPELL = CastingBlendType.create(); 8 | 9 | private CastingBlendType() { 10 | } 11 | 12 | public static CastingBlendType create() { 13 | return new CastingBlendType<>(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/blend/CastingBlender.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.blend; 2 | 3 | import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; 4 | import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.function.Function; 8 | 9 | public final class CastingBlender { 10 | public static final CastingBlender EMPTY = new CastingBlender(); 11 | 12 | private final Reference2ObjectMap, Entry> entries = new Reference2ObjectOpenHashMap<>(); 13 | 14 | @SuppressWarnings("unchecked") 15 | public Entry entry(final CastingBlendType type) { 16 | return (Entry) entries.computeIfAbsent(type, t -> new Entry<>()); 17 | } 18 | 19 | @Nullable 20 | @SuppressWarnings("unchecked") 21 | public Into loadBlendInto(final CastingBlendType type) { 22 | final Entry entry = (Entry) entries.remove(type); 23 | return entry != null ? entry.blendInto : null; 24 | } 25 | 26 | public void loadBlendOut(final CastingBlendBuilder blendBuilder) { 27 | for (final Entry entry : entries.values()) { 28 | if (entry.blendOut != null) { 29 | entry.blendOut.accept(blendBuilder); 30 | } 31 | } 32 | } 33 | 34 | public static final class Entry { 35 | private Into blendInto; 36 | private Out blendOut; 37 | 38 | public Entry blendInto(final Into blend) { 39 | blendInto = blend; 40 | return this; 41 | } 42 | 43 | public Entry blendInto(final Function blend) { 44 | return blendInto((builder, parameter) -> blend.apply(builder)); 45 | } 46 | 47 | public Entry blendOut(final Out blend) { 48 | blendOut = blend; 49 | return this; 50 | } 51 | } 52 | 53 | public interface Into { 54 | T apply(CastingBlendBuilder blend, P parameter); 55 | } 56 | 57 | public interface Out { 58 | void accept(CastingBlendBuilder blend); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/FadingGlyph.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing; 2 | 3 | import dev.gegy.magic.client.glyph.GlyphPlane; 4 | import dev.gegy.magic.client.glyph.SpellSource; 5 | import dev.gegy.magic.glyph.GlyphForm; 6 | import dev.gegy.magic.math.AnimationTimer; 7 | import dev.gegy.magic.math.Easings; 8 | 9 | public final class FadingGlyph { 10 | private final SpellSource source; 11 | private final GlyphPlane plane; 12 | private final GlyphForm form; 13 | 14 | private final AnimationTimer timer; 15 | 16 | public FadingGlyph(final SpellSource source, final GlyphPlane plane, final GlyphForm form, final AnimationTimer timer) { 17 | this.source = source; 18 | this.plane = plane; 19 | this.form = form; 20 | this.timer = timer; 21 | } 22 | 23 | public float getOpacity(final float tickDelta) { 24 | return 1.0f - Easings.easeOutCirc(timer.getProgress(tickDelta)); 25 | } 26 | 27 | public SpellSource source() { 28 | return source; 29 | } 30 | 31 | public GlyphPlane plane() { 32 | return plane; 33 | } 34 | 35 | public GlyphForm form() { 36 | return form; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/GlyphStrokeTracker.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing; 2 | 3 | import dev.gegy.magic.client.glyph.GlyphStroke; 4 | import net.minecraft.util.Mth; 5 | 6 | final class GlyphStrokeTracker { 7 | private final float fromX; 8 | private final float fromY; 9 | private float toX; 10 | private float toY; 11 | private float prevToX; 12 | private float prevToY; 13 | 14 | public GlyphStrokeTracker(final float fromX, final float fromY) { 15 | this.fromX = toX = prevToX = fromX; 16 | this.fromY = toY = prevToY = fromY; 17 | } 18 | 19 | public void tick(final float x, final float y) { 20 | prevToX = toX; 21 | prevToY = toY; 22 | toX = x; 23 | toY = y; 24 | } 25 | 26 | public GlyphStroke resolve(final float tickDelta) { 27 | final float toX = Mth.lerp(tickDelta, prevToX, this.toX); 28 | final float toY = Mth.lerp(tickDelta, prevToY, this.toY); 29 | return new GlyphStroke(fromX, fromY, toX, toY); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/input/BeginDraw.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing.input; 2 | 3 | import dev.gegy.magic.client.casting.drawing.ClientCastingDrawing; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.client.casting.drawing.input.outline.GlyphOutline; 6 | import dev.gegy.magic.client.casting.drawing.input.outline.GlyphOutlineTracker; 7 | import net.minecraft.world.entity.player.Player; 8 | 9 | final class BeginDraw implements DrawingInputState { 10 | private static final int SAMPLE_INTERVAL = 2; 11 | private static final int SAMPLE_PERIOD = 80; 12 | private static final int SAMPLE_BUFFER_SIZE = SAMPLE_PERIOD / SAMPLE_INTERVAL; 13 | 14 | private final GlyphOutlineTracker outlineTracker = new GlyphOutlineTracker(SAMPLE_BUFFER_SIZE); 15 | 16 | @Override 17 | public DrawingInputState tick(final ClientCastingDrawing casting, final Player player) { 18 | if (player.tickCount % SAMPLE_INTERVAL == 0) { 19 | final GlyphOutline outline = outlineTracker.pushSample(player.getViewVector(1.0f)); 20 | if (outline != null) { 21 | final ClientDrawingGlyph glyph = outline.createGlyph(player); 22 | casting.senders().beginGlyph(outline.plane().direction(), outline.radius()); 23 | return new DrawGlyph.OutsideCircle(glyph, outline.plane()); 24 | } 25 | } 26 | 27 | return this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/input/ContinueDraw.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing.input; 2 | 3 | import dev.gegy.magic.client.casting.drawing.ClientCastingDrawing; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.client.casting.drawing.input.outline.GlyphOutline; 6 | import dev.gegy.magic.client.casting.drawing.input.outline.GlyphOutlineTracker; 7 | import net.minecraft.world.entity.player.Player; 8 | 9 | final class ContinueDraw implements DrawingInputState { 10 | private static final int SAMPLE_INTERVAL = 2; 11 | private static final int SAMPLE_PERIOD = 80; 12 | private static final int SAMPLE_BUFFER_SIZE = SAMPLE_PERIOD / SAMPLE_INTERVAL; 13 | 14 | private final GlyphOutlineTracker outlineTracker = new GlyphOutlineTracker(SAMPLE_BUFFER_SIZE); 15 | 16 | @Override 17 | public DrawingInputState tick(final ClientCastingDrawing casting, final Player player) { 18 | // TODO: crude detection 19 | if (player.swinging) { 20 | casting.senders().prepareSpell(); 21 | return new BeginDraw(); 22 | } 23 | 24 | if (player.tickCount % SAMPLE_INTERVAL == 0) { 25 | final GlyphOutline outline = outlineTracker.pushSample(player.getViewVector(1.0f)); 26 | if (outline != null) { 27 | final ClientDrawingGlyph glyph = outline.createGlyph(player); 28 | casting.senders().beginGlyph(outline.plane().direction(), outline.radius()); 29 | return new DrawGlyph.OutsideCircle(glyph, outline.plane()); 30 | } 31 | } 32 | 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/input/DrawingCastingInput.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing.input; 2 | 3 | import dev.gegy.magic.client.casting.drawing.ClientCastingDrawing; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.glyph.GlyphType; 6 | import net.minecraft.world.entity.player.Player; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | public final class DrawingCastingInput { 11 | private DrawingInputState state; 12 | 13 | @Nullable 14 | public ClientDrawingGlyph tick(final ClientCastingDrawing casting, final Player player) { 15 | DrawingInputState state = getStateOrInit(); 16 | this.state = state = state.tick(casting, player); 17 | 18 | return state != null ? state.getDrawingGlyph() : null; 19 | } 20 | 21 | @NotNull 22 | private DrawingInputState getStateOrInit() { 23 | DrawingInputState state = this.state; 24 | if (state == null) { 25 | this.state = state = new BeginDraw(); 26 | } 27 | return state; 28 | } 29 | 30 | public void finishDrawing(final GlyphType matchedType) { 31 | final DrawingInputState state = this.state; 32 | if (state != null) { 33 | this.state = state.finishDrawingGlyph(matchedType); 34 | } 35 | } 36 | 37 | public void cancelDrawing() { 38 | final DrawingInputState state = this.state; 39 | if (state != null) { 40 | this.state = state.cancelDrawingGlyph(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/input/DrawingInputState.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing.input; 2 | 3 | import dev.gegy.magic.client.casting.drawing.ClientCastingDrawing; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.glyph.GlyphType; 6 | import net.minecraft.world.entity.player.Player; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | interface DrawingInputState { 10 | DrawingInputState tick(ClientCastingDrawing casting, Player player); 11 | 12 | default DrawingInputState finishDrawingGlyph(final GlyphType matchedType) { 13 | return this; 14 | } 15 | 16 | default DrawingInputState cancelDrawingGlyph() { 17 | return this; 18 | } 19 | 20 | @Nullable 21 | default ClientDrawingGlyph getDrawingGlyph() { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/input/outline/GlyphOutline.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing.input.outline; 2 | 3 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 4 | import dev.gegy.magic.client.glyph.GlyphPlane; 5 | import dev.gegy.magic.client.glyph.SpellSource; 6 | import net.minecraft.world.entity.player.Player; 7 | 8 | public record GlyphOutline(GlyphPlane plane, float radius) { 9 | public ClientDrawingGlyph createGlyph(final Player source) { 10 | return new ClientDrawingGlyph(SpellSource.of(source), plane, radius); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/drawing/preparing/ClientCastingPreparing.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.drawing.preparing; 2 | 3 | public final class ClientCastingPreparing { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/casting/spell/teleport/ClientCastingTeleport.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.casting.spell.teleport; 2 | 3 | import dev.gegy.magic.casting.spell.teleport.SelectTeleportTarget; 4 | import dev.gegy.magic.casting.spell.teleport.TeleportInput; 5 | import dev.gegy.magic.casting.spell.teleport.TeleportParameters; 6 | import dev.gegy.magic.casting.spell.teleport.TeleportTarget; 7 | import dev.gegy.magic.casting.spell.teleport.TeleportTargetSymbol; 8 | import dev.gegy.magic.client.casting.ClientCasting; 9 | import dev.gegy.magic.client.casting.ClientCastingBuilder; 10 | import dev.gegy.magic.client.effect.casting.spell.SpellEffects; 11 | import dev.gegy.magic.client.effect.casting.spell.teleport.TeleportEffect; 12 | import dev.gegy.magic.client.glyph.spell.Spell; 13 | import dev.gegy.magic.client.glyph.spell.SpellCastingGlyph; 14 | import dev.gegy.magic.client.glyph.spell.transform.SpellTransformType; 15 | import dev.gegy.magic.network.NetworkSender; 16 | import net.minecraft.world.entity.player.Player; 17 | 18 | import java.util.Map; 19 | import java.util.UUID; 20 | 21 | public final class ClientCastingTeleport { 22 | private final TeleportEffect effect; 23 | 24 | private final Map targets; 25 | private TeleportTarget selectedTarget; 26 | 27 | private final EventSenders eventSenders; 28 | 29 | private ClientCastingTeleport(final TeleportEffect effect, final Map targets, final EventSenders eventSenders) { 30 | this.effect = effect; 31 | this.targets = targets; 32 | this.eventSenders = eventSenders; 33 | } 34 | 35 | public static ClientCasting build(final Player player, final TeleportParameters parameters, final ClientCastingBuilder casting) { 36 | final Spell spell = Spell.blendOrCreate(player, casting, parameters.spell(), SpellTransformType.FIXED); 37 | 38 | final SpellCastingGlyph sourceGlyph = spell.glyphs().get(0); 39 | 40 | final TeleportEffect effect = casting.attachEffect(TeleportEffect.create( 41 | sourceGlyph, 42 | parameters.targets().values(), 43 | player.level().getGameTime() 44 | )); 45 | SpellEffects.attach(spell, casting); 46 | 47 | final EventSenders eventSenders = EventSenders.register(casting); 48 | 49 | final ClientCastingTeleport teleport = new ClientCastingTeleport(effect, parameters.targets(), eventSenders); 50 | 51 | casting.registerTicker(teleport::tick); 52 | 53 | if (player.isLocalPlayer()) { 54 | teleport.bindInput(player, casting); 55 | } 56 | 57 | return casting.build(); 58 | } 59 | 60 | private void bindInput(final Player player, final ClientCastingBuilder casting) { 61 | final TeleportInput input = new TeleportInput(); 62 | casting.registerTicker(() -> { 63 | final TeleportTarget target = input.tick(this, player); 64 | if (target != null) { 65 | selectTarget(target); 66 | } 67 | }); 68 | } 69 | 70 | private void tick() { 71 | 72 | } 73 | 74 | private void selectTarget(final TeleportTarget target) { 75 | selectedTarget = target; 76 | eventSenders.selectTarget(target); 77 | } 78 | 79 | private record EventSenders(NetworkSender selectTarget) { 80 | private static EventSenders register(final ClientCastingBuilder casting) { 81 | return new EventSenders( 82 | casting.registerOutboundEvent(SelectTeleportTarget.SPEC) 83 | ); 84 | } 85 | 86 | public void selectTarget(final TeleportTarget target) { 87 | selectTarget.send(new SelectTeleportTarget(target.id())); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/Effect.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect; 2 | 3 | public interface Effect { 4 | EffectType getType(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/EffectMap.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect; 2 | 3 | import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.AbstractCollection; 8 | import java.util.Iterator; 9 | import java.util.Map; 10 | 11 | public final class EffectMap extends AbstractCollection implements EffectSelector { 12 | private final Map, Effect> effects = new Reference2ObjectOpenHashMap<>(); 13 | 14 | @Override 15 | public boolean add(final Effect effect) { 16 | return effects.putIfAbsent(effect.getType(), effect) == null; 17 | } 18 | 19 | @Override 20 | public boolean remove(final Object o) { 21 | return o instanceof Effect effect && remove(effect); 22 | } 23 | 24 | public boolean remove(final Effect effect) { 25 | return effects.remove(effect.getType(), effect); 26 | } 27 | 28 | @Override 29 | public void clear() { 30 | effects.clear(); 31 | } 32 | 33 | @SuppressWarnings("unchecked") 34 | @Nullable 35 | public E get(final EffectType type) { 36 | return (E) effects.get(type); 37 | } 38 | 39 | @SuppressWarnings("unchecked") 40 | @Nullable 41 | public E remove(final EffectType type) { 42 | return (E) effects.remove(type); 43 | } 44 | 45 | @Override 46 | public boolean contains(final Object o) { 47 | return o instanceof Effect effect && contains(effect); 48 | } 49 | 50 | public boolean contains(final Effect effect) { 51 | return effects.get(effect.getType()) == effect; 52 | } 53 | 54 | public boolean contains(final EffectType type) { 55 | return effects.containsKey(type); 56 | } 57 | 58 | @Override 59 | public Selection select(final EffectType type) { 60 | final E effect = get(type); 61 | if (effect != null) { 62 | return Selection.singleton(effect); 63 | } else { 64 | return Selection.empty(); 65 | } 66 | } 67 | 68 | @Override 69 | @Nullable 70 | public E selectAny(final EffectType type) { 71 | return get(type); 72 | } 73 | 74 | @NotNull 75 | @Override 76 | public Iterator iterator() { 77 | return effects.values().iterator(); 78 | } 79 | 80 | @Override 81 | public int size() { 82 | return effects.size(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/EffectSelector.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect; 2 | 3 | import com.google.common.collect.Iterators; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Collections; 7 | import java.util.Iterator; 8 | 9 | public interface EffectSelector extends Iterable { 10 | EffectSelector EMPTY = new EffectSelector() { 11 | @Override 12 | public Selection select(final EffectType type) { 13 | return Selection.empty(); 14 | } 15 | 16 | @Override 17 | public Iterator iterator() { 18 | return Collections.emptyIterator(); 19 | } 20 | }; 21 | 22 | Selection select(EffectType type); 23 | 24 | @Nullable 25 | default E selectAny(final EffectType type) { 26 | final Selection selection = select(type); 27 | final Iterator iterator = selection.iterator(); 28 | return iterator.hasNext() ? iterator.next() : null; 29 | } 30 | 31 | default boolean has(final EffectType type) { 32 | return !select(type).isEmpty(); 33 | } 34 | 35 | @Override 36 | Iterator iterator(); 37 | 38 | interface Selection extends Iterable { 39 | static Selection singleton(final E effect) { 40 | return new Selection<>() { 41 | @Override 42 | public boolean isEmpty() { 43 | return false; 44 | } 45 | 46 | @Override 47 | public Iterator iterator() { 48 | return Iterators.singletonIterator(effect); 49 | } 50 | }; 51 | } 52 | 53 | static Selection empty() { 54 | return new Selection<>() { 55 | @Override 56 | public boolean isEmpty() { 57 | return true; 58 | } 59 | 60 | @Override 61 | public Iterator iterator() { 62 | return Collections.emptyIterator(); 63 | } 64 | }; 65 | } 66 | 67 | boolean isEmpty(); 68 | 69 | @Override 70 | Iterator iterator(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/EffectSystem.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect; 2 | 3 | import com.mojang.blaze3d.pipeline.RenderTarget; 4 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.server.packs.resources.ResourceManager; 7 | 8 | import java.io.IOException; 9 | 10 | public interface EffectSystem extends AutoCloseable { 11 | void render(Minecraft client, WorldRenderContext context, RenderTarget targetFramebuffer, EffectSelector effects); 12 | 13 | default void tick(final Minecraft client, final EffectSelector effects) { 14 | } 15 | 16 | @Override 17 | void close(); 18 | 19 | interface Factory { 20 | E create(ResourceManager resources) throws IOException; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/EffectType.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect; 2 | 3 | public final class EffectType { 4 | private EffectType() { 5 | } 6 | 7 | public static EffectType create() { 8 | return new EffectType<>(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/GlobalEffectList.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect; 2 | 3 | import com.google.common.collect.Iterables; 4 | import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; 5 | 6 | import java.util.AbstractCollection; 7 | import java.util.ArrayList; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public final class GlobalEffectList extends AbstractCollection { 13 | private final Map, List> effectsByType = new Reference2ObjectOpenHashMap<>(); 14 | 15 | private final Selector selector = new Selector(); 16 | 17 | public EffectSelector selector() { 18 | return selector; 19 | } 20 | 21 | @Override 22 | public boolean add(final Effect effect) { 23 | getEffectsLike(effect).add(effect); 24 | return true; 25 | } 26 | 27 | @Override 28 | public boolean remove(final Object o) { 29 | return o instanceof Effect effect && remove(effect); 30 | } 31 | 32 | private boolean remove(final Effect effect) { 33 | return getEffectsLike(effect).remove(effect); 34 | } 35 | 36 | @Override 37 | public Iterator iterator() { 38 | return Iterables.concat(effectsByType.values()).iterator(); 39 | } 40 | 41 | @Override 42 | public int size() { 43 | int size = 0; 44 | for (final List effects : effectsByType.values()) { 45 | size += effects.size(); 46 | } 47 | return size; 48 | } 49 | 50 | @SuppressWarnings("unchecked") 51 | private List getEffectsLike(final E effect) { 52 | return (List) getEffectsByType(effect.getType()); 53 | } 54 | 55 | @SuppressWarnings("unchecked") 56 | private List getEffectsByType(final EffectType type) { 57 | return (List) effectsByType.computeIfAbsent(type, t -> new ArrayList<>()); 58 | } 59 | 60 | private final class Selector implements EffectSelector { 61 | @Override 62 | public Selection select(final EffectType type) { 63 | final List effects = getEffectsByType(type); 64 | return new Selection<>() { 65 | @Override 66 | public boolean isEmpty() { 67 | return effects.isEmpty(); 68 | } 69 | 70 | @Override 71 | public Iterator iterator() { 72 | return effects.iterator(); 73 | } 74 | }; 75 | } 76 | 77 | @Override 78 | public Iterator iterator() { 79 | return GlobalEffectList.this.iterator(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/drawing/DrawingEffect.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.drawing; 2 | 3 | import dev.gegy.magic.client.casting.drawing.ClientCastingDrawing; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.client.effect.Effect; 6 | import dev.gegy.magic.client.effect.EffectType; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | public record DrawingEffect(ClientCastingDrawing casting) implements Effect { 10 | public static final EffectType TYPE = EffectType.create(); 11 | 12 | @Nullable 13 | public ClientDrawingGlyph getGlyph() { 14 | return casting.getDrawing(); 15 | } 16 | 17 | @Override 18 | public EffectType getType() { 19 | return TYPE; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/PreparedSpellEffect.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell; 2 | 3 | import dev.gegy.magic.client.effect.Effect; 4 | import dev.gegy.magic.client.effect.EffectType; 5 | import dev.gegy.magic.client.glyph.spell.Spell; 6 | 7 | public record PreparedSpellEffect(Spell spell) implements Effect { 8 | public static final EffectType TYPE = EffectType.create(); 9 | 10 | @Override 11 | public EffectType getType() { 12 | return TYPE; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/SpellEffects.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell; 2 | 3 | import dev.gegy.magic.client.casting.ClientCastingBuilder; 4 | import dev.gegy.magic.client.effect.glyph.GlyphsEffect; 5 | import dev.gegy.magic.client.glyph.spell.Spell; 6 | 7 | public final class SpellEffects { 8 | public static void attach(final Spell spell, final ClientCastingBuilder casting) { 9 | casting.attachEffect(new PreparedSpellEffect(spell)); 10 | casting.attachEffect(GlyphsEffect.fromSpell(spell)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/beam/BeamEffect.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.beam; 2 | 3 | import dev.gegy.magic.client.effect.Effect; 4 | import dev.gegy.magic.client.effect.EffectType; 5 | import dev.gegy.magic.client.glyph.spell.Spell; 6 | import dev.gegy.magic.math.ColorRgb; 7 | import net.minecraft.util.Mth; 8 | 9 | public final class BeamEffect implements Effect { 10 | public static final EffectType TYPE = EffectType.create(); 11 | 12 | private final Spell spell; 13 | private final ColorRgb color = ColorRgb.of(1.0f, 0.3f, 0.3f); 14 | 15 | private float prevLength; 16 | private float length; 17 | 18 | private boolean visible; 19 | 20 | public BeamEffect(final Spell spell) { 21 | this.spell = spell; 22 | } 23 | 24 | public void tick(final float length) { 25 | prevLength = Math.min(length, this.length); 26 | this.length = length; 27 | } 28 | 29 | public void setVisible(final boolean visible) { 30 | this.visible = visible; 31 | } 32 | 33 | public Spell spell() { 34 | return spell; 35 | } 36 | 37 | public ColorRgb color() { 38 | return color; 39 | } 40 | 41 | public float getLength(final float tickDelta) { 42 | return Mth.lerp(tickDelta, prevLength, length); 43 | } 44 | 45 | public boolean visible() { 46 | return visible; 47 | } 48 | 49 | @Override 50 | public EffectType getType() { 51 | return TYPE; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/beam/render/BeamEndWorldShader.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.beam.render; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import dev.gegy.magic.Magic; 5 | import dev.gegy.magic.client.effect.shader.EffectShader; 6 | import dev.gegy.magic.client.effect.shader.EffectShaderProgram; 7 | import dev.gegy.magic.client.render.GeometryBuilder; 8 | import dev.gegy.magic.client.render.gl.GlBinding; 9 | import net.minecraft.server.packs.resources.ResourceManager; 10 | import org.joml.Matrix4f; 11 | import org.lwjgl.opengl.GL20; 12 | import org.lwjgl.system.MemoryUtil; 13 | 14 | import java.io.IOException; 15 | import java.nio.FloatBuffer; 16 | import java.util.function.Function; 17 | 18 | final class BeamEndWorldShader implements EffectShader { 19 | private final EffectShaderProgram program; 20 | 21 | private final int uniformModelViewProject; 22 | private final int uniformSampler; 23 | private final int uniformScale; 24 | 25 | private final Function modelViewProject; 26 | 27 | private final FloatBuffer modelViewProjectData = MemoryUtil.memAllocFloat(4 * 4); 28 | 29 | private BeamEndWorldShader( 30 | final EffectShaderProgram program, 31 | final int uniformModelViewProject, 32 | final int uniformSampler, 33 | final int uniformScale, 34 | final Function modelViewProject 35 | ) { 36 | this.program = program; 37 | this.uniformModelViewProject = uniformModelViewProject; 38 | this.uniformSampler = uniformSampler; 39 | this.uniformScale = uniformScale; 40 | this.modelViewProject = modelViewProject; 41 | } 42 | 43 | public static BeamEndWorldShader create(final ResourceManager resources, final Function modelViewProject) throws IOException { 44 | final EffectShaderProgram program = EffectShaderProgram.compile( 45 | resources, 46 | Magic.identifier("beam/end_world"), 47 | Magic.identifier("effect_world"), 48 | GeometryBuilder.POSITION_2F 49 | ); 50 | 51 | final int uniformModelViewProject = program.getUniformLocation("ModelViewProject"); 52 | final int uniformSampler = program.getUniformLocation("Sampler"); 53 | final int uniformScale = program.getUniformLocation("Scale"); 54 | 55 | return new BeamEndWorldShader( 56 | program, 57 | uniformModelViewProject, 58 | uniformSampler, 59 | uniformScale, 60 | modelViewProject 61 | ); 62 | } 63 | 64 | @Override 65 | public GlBinding bind(final BeamRenderParameters parameters) { 66 | final EffectShaderProgram.Binding binding = program.bind(); 67 | 68 | RenderSystem.glUniform1i(uniformSampler, 0); 69 | 70 | RenderSystem.glUniformMatrix4(uniformModelViewProject, false, 71 | modelViewProject.apply(parameters).get(modelViewProjectData) 72 | ); 73 | 74 | GL20.glUniform1f(uniformScale, BeamTexture.END_SCALE); 75 | 76 | return binding; 77 | } 78 | 79 | @Override 80 | public void delete() { 81 | program.delete(); 82 | 83 | MemoryUtil.memFree(modelViewProjectData); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/beam/render/BeamRenderParameters.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.beam.render; 2 | 3 | import dev.gegy.magic.client.effect.casting.spell.beam.BeamEffect; 4 | import dev.gegy.magic.client.glyph.GlyphPlane; 5 | import dev.gegy.magic.client.glyph.spell.Spell; 6 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 7 | import net.minecraft.world.phys.Vec3; 8 | import org.joml.Matrix4f; 9 | import org.joml.Vector4f; 10 | 11 | public final class BeamRenderParameters { 12 | private static final long DAY_LENGTH = 24000; 13 | 14 | public final Matrix4f modelViewProject = new Matrix4f(); 15 | public final Matrix4f cloudModelViewProject = new Matrix4f(); 16 | public final Matrix4f impactModelViewProject = new Matrix4f(); 17 | 18 | public float red, green, blue; 19 | public float time; 20 | 21 | public float length; 22 | 23 | private final Matrix4f viewProject = new Matrix4f(); 24 | private final Vector4f endPoint = new Vector4f(); 25 | 26 | private final GlyphPlane plane = new GlyphPlane(); 27 | 28 | public void set(final BeamEffect beam, final WorldRenderContext context) { 29 | final float tickDelta = context.tickDelta(); 30 | 31 | final Spell spell = beam.spell(); 32 | final GlyphPlane plane = this.plane; 33 | plane.set(spell.transform(), tickDelta); 34 | 35 | final float length = beam.getLength(context.tickDelta()); 36 | this.length = length; 37 | 38 | final Matrix4f viewProject = computeViewProject(spell, context); 39 | 40 | final Matrix4f glyphTransform = plane.planeToWorld(); 41 | 42 | modelViewProject.set(viewProject).mul(glyphTransform); 43 | 44 | setCloudModelViewProject(viewProject, glyphTransform); 45 | setImpactModelViewProject(context, viewProject, glyphTransform, length); 46 | 47 | red = beam.color().red(); 48 | green = beam.color().green(); 49 | blue = beam.color().blue(); 50 | 51 | final float glyphTime = context.world().getGameTime() % DAY_LENGTH; 52 | time = (glyphTime + tickDelta) / 20.0f; 53 | } 54 | 55 | private void setCloudModelViewProject(final Matrix4f viewProject, final Matrix4f spellTransform) { 56 | cloudModelViewProject.set(viewProject) 57 | .mul(spellTransform) 58 | .translate(0.0f, 0.0f, 0.8f); 59 | } 60 | 61 | private void setImpactModelViewProject(final WorldRenderContext context, final Matrix4f viewProject, final Matrix4f spellTransform, final float length) { 62 | final Vector4f point = endPoint.set(0.0f, 0.0f, length, 1.0f).mul(spellTransform); 63 | 64 | impactModelViewProject.set(viewProject) 65 | .translate(point.x(), point.y(), point.z()) 66 | .rotate(context.camera().rotation()); 67 | } 68 | 69 | private Matrix4f computeViewProject(final Spell spell, final WorldRenderContext context) { 70 | final Matrix4f modelMatrix = context.matrixStack().last().pose(); 71 | final Vec3 cameraPos = context.camera().getPosition(); 72 | 73 | final Vec3 sourcePos = spell.source().getPosition(context.tickDelta()); 74 | 75 | return viewProject.set(context.projectionMatrix()) 76 | .mul(modelMatrix) 77 | .translate( 78 | (float) (sourcePos.x - cameraPos.x), 79 | (float) (sourcePos.y - cameraPos.y), 80 | (float) (sourcePos.z - cameraPos.z) 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/beam/render/BeamTexture.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.beam.render; 2 | 3 | import dev.gegy.magic.casting.spell.beam.ServerCastingBeam; 4 | 5 | final class BeamTexture { 6 | public static final int RESOLUTION = 8; 7 | 8 | public static final int WIDTH = RESOLUTION * ServerCastingBeam.MAXIMUM_LENGTH; 9 | public static final int HEIGHT = RESOLUTION * 2; 10 | public static final float SCALE_X = (float) WIDTH / RESOLUTION; 11 | public static final float SCALE_Y = (float) HEIGHT / RESOLUTION; 12 | 13 | public static final int END_RESOLUTION = 16; 14 | public static final int END_SIZE = END_RESOLUTION * 4; 15 | public static final float END_SCALE = (float) END_SIZE / END_RESOLUTION; 16 | 17 | private BeamTexture() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/beam/render/BeamWorldShader.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.beam.render; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import com.mojang.blaze3d.vertex.DefaultVertexFormat; 5 | import dev.gegy.magic.Magic; 6 | import dev.gegy.magic.client.effect.shader.EffectShader; 7 | import dev.gegy.magic.client.effect.shader.EffectShaderProgram; 8 | import dev.gegy.magic.client.render.gl.GlBinding; 9 | import net.minecraft.server.packs.resources.ResourceManager; 10 | import org.lwjgl.system.MemoryUtil; 11 | 12 | import java.io.IOException; 13 | import java.nio.FloatBuffer; 14 | 15 | final class BeamWorldShader implements EffectShader { 16 | private final EffectShaderProgram program; 17 | 18 | private final int uniformModelViewProject; 19 | private final int uniformSampler; 20 | private final int uniformScale; 21 | 22 | private final FloatBuffer scaleData = MemoryUtil.memAllocFloat(2); 23 | private final FloatBuffer modelViewProjectData = MemoryUtil.memAllocFloat(4 * 4); 24 | 25 | private BeamWorldShader( 26 | final EffectShaderProgram program, 27 | final int uniformModelViewProject, 28 | final int uniformSampler, 29 | final int uniformScale 30 | ) { 31 | this.program = program; 32 | this.uniformModelViewProject = uniformModelViewProject; 33 | this.uniformSampler = uniformSampler; 34 | this.uniformScale = uniformScale; 35 | } 36 | 37 | public static BeamWorldShader create(final ResourceManager resources) throws IOException { 38 | final EffectShaderProgram program = EffectShaderProgram.compile( 39 | resources, 40 | Magic.identifier("beam/world"), 41 | Magic.identifier("effect_world"), 42 | DefaultVertexFormat.POSITION_TEX 43 | ); 44 | 45 | final int uniformModelViewProject = program.getUniformLocation("ModelViewProject"); 46 | final int uniformSampler = program.getUniformLocation("Sampler"); 47 | final int uniformScale = program.getUniformLocation("Scale"); 48 | 49 | return new BeamWorldShader( 50 | program, 51 | uniformModelViewProject, 52 | uniformSampler, 53 | uniformScale 54 | ); 55 | } 56 | 57 | @Override 58 | public GlBinding bind(final BeamRenderParameters parameters) { 59 | final EffectShaderProgram.Binding binding = program.bind(); 60 | 61 | RenderSystem.glUniform1i(uniformSampler, 0); 62 | 63 | RenderSystem.glUniformMatrix4(uniformModelViewProject, false, 64 | parameters.modelViewProject.get(modelViewProjectData) 65 | ); 66 | RenderSystem.glUniform2(uniformScale, 67 | scaleData.put(0, BeamTexture.SCALE_X).put(1, BeamTexture.SCALE_Y) 68 | ); 69 | 70 | return binding; 71 | } 72 | 73 | @Override 74 | public void delete() { 75 | program.delete(); 76 | 77 | MemoryUtil.memFree(scaleData); 78 | MemoryUtil.memFree(modelViewProjectData); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/teleport/TeleportEffect.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.teleport; 2 | 3 | import dev.gegy.magic.casting.spell.teleport.TeleportTargetSymbol; 4 | import dev.gegy.magic.client.effect.Effect; 5 | import dev.gegy.magic.client.effect.EffectType; 6 | import dev.gegy.magic.client.glyph.GlyphPlane; 7 | import dev.gegy.magic.client.glyph.SpellSource; 8 | import dev.gegy.magic.client.glyph.spell.SpellCastingGlyph; 9 | import dev.gegy.magic.math.ColorHsluv; 10 | import dev.gegy.magic.math.ColorRgb; 11 | import net.minecraft.client.Minecraft; 12 | import net.minecraft.client.gui.Font; 13 | import net.minecraft.network.chat.Style; 14 | import net.minecraft.util.FormattedCharSequence; 15 | 16 | import java.util.Collection; 17 | import java.util.List; 18 | 19 | public record TeleportEffect( 20 | SpellSource source, 21 | GlyphPlane sourcePlane, 22 | List symbols, 23 | TeleportTargetAnimator animator, 24 | long createTime 25 | ) implements Effect { 26 | public static final float SYMBOL_SIZE = 0.5f; 27 | 28 | public static final EffectType TYPE = EffectType.create(); 29 | 30 | public static TeleportEffect create(final SpellCastingGlyph sourceGlyph, final Collection targets, final long createTime) { 31 | final float sourceRadius = sourceGlyph.form().radius(); 32 | 33 | final GlyphPlane sourcePlane = new GlyphPlane(); 34 | sourcePlane.set(sourceGlyph.transform()); 35 | 36 | final List symbols = targets.stream().map(Symbol::create).toList(); 37 | final TeleportTargetAnimator animator = new TeleportTargetAnimator(sourceRadius + (SYMBOL_SIZE / 2.0f), symbols.size()); 38 | 39 | return new TeleportEffect(sourceGlyph.source(), sourcePlane, symbols, animator, createTime); 40 | } 41 | 42 | @Override 43 | public EffectType getType() { 44 | return TYPE; 45 | } 46 | 47 | public record Symbol( 48 | FormattedCharSequence text, 49 | ColorRgb innerColor, ColorRgb outlineColor, 50 | float offsetX, float offsetY 51 | ) { 52 | private static final Minecraft CLIENT = Minecraft.getInstance(); 53 | 54 | static Symbol create(final TeleportTargetSymbol symbol) { 55 | final Font textRenderer = CLIENT.font; 56 | 57 | final FormattedCharSequence text = FormattedCharSequence.codepoint(symbol.character(), Style.EMPTY); 58 | 59 | final ColorRgb innerColor = getInnerColor(symbol.color()); 60 | final ColorRgb outerColor = getOuterColor(symbol.color()); 61 | 62 | final float offsetX = -textRenderer.width(text) / 2.0f; 63 | final float offsetY = -textRenderer.lineHeight / 2.0f; 64 | 65 | return new Symbol(text, innerColor, outerColor, offsetX, offsetY); 66 | } 67 | 68 | private static ColorRgb getInnerColor(final ColorRgb color) { 69 | ColorHsluv hsluv = color.toHsluv(); 70 | hsluv = hsluv.warmHue(30.0f / 360.0f) 71 | .withSaturation(0.6f) 72 | .withLight(0.85f); 73 | return hsluv.toRgb(); 74 | } 75 | 76 | private static ColorRgb getOuterColor(final ColorRgb color) { 77 | ColorHsluv hsluv = color.toHsluv(); 78 | hsluv = hsluv 79 | .withSaturation(0.9f) 80 | .withLight(0.55f); 81 | return hsluv.toRgb(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/teleport/TeleportEffectRenderer.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.teleport; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.gui.Font; 7 | import net.minecraft.client.renderer.LightTexture; 8 | import net.minecraft.util.Mth; 9 | import net.minecraft.world.phys.Vec2; 10 | import net.minecraft.world.phys.Vec3; 11 | import org.joml.Matrix4f; 12 | 13 | import java.util.List; 14 | 15 | public final class TeleportEffectRenderer { 16 | public void render(final Minecraft client, final WorldRenderContext context, final TeleportEffect effect) { 17 | final Font textRenderer = client.font; 18 | 19 | final long worldTime = context.world().getGameTime(); 20 | final float tickDelta = context.tickDelta(); 21 | 22 | final Vec3 sourcePos = effect.source().getPosition(tickDelta); 23 | final Vec3 cameraPos = context.camera().getPosition(); 24 | 25 | final float time = (float) (worldTime - effect.createTime()) + tickDelta; 26 | 27 | final TeleportTargetAnimator animator = effect.animator(); 28 | final List targets = effect.symbols(); 29 | 30 | final PoseStack matrixStack = context.matrixStack(); 31 | matrixStack.pushPose(); 32 | matrixStack.translate( 33 | (float) (sourcePos.x - cameraPos.x), 34 | (float) (sourcePos.y - cameraPos.y), 35 | (float) (sourcePos.z - cameraPos.z) 36 | ); 37 | matrixStack.mulPoseMatrix(effect.sourcePlane().planeToWorld()); 38 | 39 | final float scale = 0.0625f * TeleportEffect.SYMBOL_SIZE; 40 | matrixStack.scale(-scale, -scale, scale); 41 | 42 | for (int index = 0; index < targets.size(); index++) { 43 | final TeleportEffect.Symbol target = targets.get(index); 44 | final Vec2 position = animator.getPosition(index, time); 45 | final float opacity = animator.getOpacity(index, time); 46 | if (opacity <= 0.0f) { 47 | continue; 48 | } 49 | 50 | final Matrix4f matrix = matrixStack.last().pose(); 51 | 52 | final float x = (position.x / scale) + target.offsetX(); 53 | final float y = (position.y / scale) + target.offsetY(); 54 | 55 | final int alpha = Mth.floor(opacity * 255.0f) << 24; 56 | final int innerColor = target.innerColor().packed() | alpha; 57 | final int outlineColor = target.outlineColor().packed() | alpha; 58 | 59 | textRenderer.drawInBatch8xOutline( 60 | target.text(), 61 | x, y, 62 | innerColor, outlineColor, 63 | matrix, context.consumers(), 64 | LightTexture.FULL_BRIGHT 65 | ); 66 | } 67 | 68 | matrixStack.popPose(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/casting/spell/teleport/TeleportEffectSystem.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.casting.spell.teleport; 2 | 3 | import com.mojang.blaze3d.pipeline.RenderTarget; 4 | import com.mojang.blaze3d.systems.RenderSystem; 5 | import dev.gegy.magic.client.effect.EffectSelector; 6 | import dev.gegy.magic.client.effect.EffectSystem; 7 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.server.packs.resources.ResourceManager; 10 | 11 | import java.io.IOException; 12 | 13 | public final class TeleportEffectSystem implements EffectSystem { 14 | private final TeleportEffectRenderer renderer = new TeleportEffectRenderer(); 15 | 16 | public static TeleportEffectSystem create(final ResourceManager resources) throws IOException { 17 | return new TeleportEffectSystem(); 18 | } 19 | 20 | @Override 21 | public void render(final Minecraft client, final WorldRenderContext context, final RenderTarget targetFramebuffer, final EffectSelector effects) { 22 | RenderSystem.disableCull(); 23 | 24 | for (final TeleportEffect effect : effects.select(TeleportEffect.TYPE)) { 25 | renderer.render(client, context, effect); 26 | } 27 | 28 | RenderSystem.enableCull(); 29 | } 30 | 31 | @Override 32 | public void tick(final Minecraft client, final EffectSelector effects) { 33 | } 34 | 35 | @Override 36 | public void close() { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/glyph/GlyphEffectRenderer.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.glyph; 2 | 3 | import com.mojang.blaze3d.pipeline.RenderTarget; 4 | import com.mojang.blaze3d.platform.GlStateManager; 5 | import com.mojang.blaze3d.systems.RenderSystem; 6 | import dev.gegy.magic.client.effect.shader.EffectTexture; 7 | import dev.gegy.magic.client.render.GeometryBuilder; 8 | import dev.gegy.magic.client.render.gl.GlBinding; 9 | import dev.gegy.magic.client.render.gl.GlGeometry; 10 | import net.minecraft.server.packs.resources.ResourceManager; 11 | 12 | import java.io.IOException; 13 | 14 | public final class GlyphEffectRenderer implements AutoCloseable { 15 | private final GlyphWorldShader worldShader; 16 | private final GlGeometry geometry; 17 | 18 | private final EffectTexture texture; 19 | 20 | private final Batch batch = new Batch(); 21 | 22 | private GlyphEffectRenderer(final GlyphWorldShader worldShader, final GlGeometry geometry, final EffectTexture texture) { 23 | this.worldShader = worldShader; 24 | this.geometry = geometry; 25 | this.texture = texture; 26 | } 27 | 28 | public static GlyphEffectRenderer create(final ResourceManager resources) throws IOException { 29 | final GlyphWorldShader worldShader = GlyphWorldShader.create(resources); 30 | final GlyphTextureShader textureShader = GlyphTextureShader.create(resources); 31 | final GlGeometry geometry = GeometryBuilder.uploadQuadPos2f(-1.0f, 1.0f); 32 | final EffectTexture texture = EffectTexture.create(textureShader, GlyphTexture.SIZE); 33 | 34 | return new GlyphEffectRenderer(worldShader, geometry, texture); 35 | } 36 | 37 | public Batch startBatch(final RenderTarget target) { 38 | final Batch batch = this.batch; 39 | batch.start(target); 40 | return batch; 41 | } 42 | 43 | @Override 44 | public void close() { 45 | worldShader.delete(); 46 | geometry.delete(); 47 | texture.delete(); 48 | } 49 | 50 | public final class Batch implements AutoCloseable { 51 | private RenderTarget target; 52 | private GlGeometry.Binding geometryBinding; 53 | 54 | void start(final RenderTarget target) { 55 | this.target = target; 56 | 57 | RenderSystem.disableCull(); 58 | RenderSystem.enableBlend(); 59 | RenderSystem.enableDepthTest(); 60 | RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); 61 | 62 | geometryBinding = geometry.bind(); 63 | } 64 | 65 | public void render(final GlyphRenderParameters parameters) { 66 | texture.renderWith(parameters, geometryBinding); 67 | 68 | target.bindWrite(true); 69 | renderToWorld(parameters); 70 | } 71 | 72 | private void renderToWorld(final GlyphRenderParameters parameters) { 73 | try ( 74 | final EffectTexture.ReadBinding textureBinding = texture.bindRead(); 75 | final GlBinding shaderBinding = worldShader.bind(parameters) 76 | ) { 77 | geometryBinding.draw(); 78 | } 79 | } 80 | 81 | @Override 82 | public void close() { 83 | geometryBinding.unbind(); 84 | 85 | RenderSystem.enableCull(); 86 | RenderSystem.disableBlend(); 87 | RenderSystem.disableDepthTest(); 88 | 89 | geometryBinding = null; 90 | target = null; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/glyph/GlyphEffectSystem.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.glyph; 2 | 3 | import com.mojang.blaze3d.pipeline.RenderTarget; 4 | import dev.gegy.magic.client.effect.EffectSelector; 5 | import dev.gegy.magic.client.effect.EffectSystem; 6 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.server.packs.resources.ResourceManager; 9 | 10 | import java.io.IOException; 11 | 12 | public final class GlyphEffectSystem implements EffectSystem { 13 | private final GlyphEffectRenderer renderer; 14 | private final Frame frame; 15 | 16 | private final GlyphRenderParameters parameters = new GlyphRenderParameters(); 17 | 18 | private GlyphEffectSystem(final GlyphEffectRenderer renderer) { 19 | this.renderer = renderer; 20 | frame = new Frame(renderer); 21 | } 22 | 23 | public static GlyphEffectSystem create(final ResourceManager resources) throws IOException { 24 | final GlyphEffectRenderer renderer = GlyphEffectRenderer.create(resources); 25 | return new GlyphEffectSystem(renderer); 26 | } 27 | 28 | @Override 29 | public void render(final Minecraft client, final WorldRenderContext context, final RenderTarget targetFramebuffer, final EffectSelector effects) { 30 | // TODO: frustum culling 31 | 32 | try (final Frame frame = this.frame.setup(targetFramebuffer)) { 33 | final GlyphRenderParameters parameters = this.parameters; 34 | for (final GlyphsEffect effect : effects.select(GlyphsEffect.TYPE)) { 35 | effect.render(parameters, context, frame); 36 | } 37 | } 38 | } 39 | 40 | @Override 41 | public void close() { 42 | renderer.close(); 43 | } 44 | 45 | private static final class Frame implements GlyphsEffect.RenderFunction, AutoCloseable { 46 | private final GlyphEffectRenderer renderer; 47 | 48 | private RenderTarget target; 49 | private GlyphEffectRenderer.Batch batch; 50 | 51 | public Frame(final GlyphEffectRenderer renderer) { 52 | this.renderer = renderer; 53 | } 54 | 55 | public Frame setup(final RenderTarget target) { 56 | this.target = target; 57 | return this; 58 | } 59 | 60 | @Override 61 | public void accept(final GlyphRenderParameters parameters) { 62 | GlyphEffectRenderer.Batch batch = this.batch; 63 | if (batch == null) { 64 | this.batch = batch = renderer.startBatch(target); 65 | } 66 | 67 | batch.render(parameters); 68 | } 69 | 70 | @Override 71 | public void close() { 72 | final GlyphEffectRenderer.Batch batch = this.batch; 73 | if (batch != null) { 74 | batch.close(); 75 | } 76 | 77 | target = null; 78 | this.batch = null; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/glyph/GlyphTexture.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.glyph; 2 | 3 | final class GlyphTexture { 4 | public static final int RESOLUTION = 32; 5 | 6 | public static final int SIZE = RESOLUTION * 2; 7 | public static final float TEXEL_SIZE = 1.0f / SIZE; 8 | 9 | public static final float RENDER_SIZE = RESOLUTION; 10 | public static final float RENDER_SCALE = SIZE / RENDER_SIZE; 11 | 12 | private GlyphTexture() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/glyph/GlyphWorldShader.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.glyph; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import dev.gegy.magic.Magic; 5 | import dev.gegy.magic.client.effect.shader.EffectShader; 6 | import dev.gegy.magic.client.effect.shader.EffectShaderProgram; 7 | import dev.gegy.magic.client.render.GeometryBuilder; 8 | import dev.gegy.magic.client.render.gl.GlBinding; 9 | import net.minecraft.server.packs.resources.ResourceManager; 10 | import org.lwjgl.opengl.GL20; 11 | import org.lwjgl.system.MemoryUtil; 12 | 13 | import java.io.IOException; 14 | import java.nio.FloatBuffer; 15 | 16 | final class GlyphWorldShader implements EffectShader { 17 | private final EffectShaderProgram program; 18 | 19 | private final int uniformModelViewProject; 20 | private final int uniformScale; 21 | private final int uniformSampler; 22 | 23 | private final FloatBuffer modelViewProjectData = MemoryUtil.memAllocFloat(4 * 4); 24 | 25 | private GlyphWorldShader( 26 | final EffectShaderProgram program, 27 | final int uniformModelViewProject, 28 | final int uniformScale, 29 | final int uniformSampler 30 | ) { 31 | this.program = program; 32 | this.uniformModelViewProject = uniformModelViewProject; 33 | this.uniformScale = uniformScale; 34 | this.uniformSampler = uniformSampler; 35 | } 36 | 37 | public static GlyphWorldShader create(final ResourceManager resources) throws IOException { 38 | final EffectShaderProgram program = EffectShaderProgram.compile(resources, Magic.identifier("glyph/world"), Magic.identifier("effect_world"), GeometryBuilder.POSITION_2F); 39 | 40 | final int uniformModelViewProject = program.getUniformLocation("ModelViewProject"); 41 | final int uniformScale = program.getUniformLocation("Scale"); 42 | final int uniformSampler = program.getUniformLocation("Sampler"); 43 | 44 | return new GlyphWorldShader( 45 | program, 46 | uniformModelViewProject, 47 | uniformScale, 48 | uniformSampler 49 | ); 50 | } 51 | 52 | @Override 53 | public GlBinding bind(final GlyphRenderParameters parameters) { 54 | final EffectShaderProgram.Binding binding = program.bind(); 55 | 56 | RenderSystem.glUniform1i(uniformSampler, 0); 57 | 58 | RenderSystem.glUniformMatrix4(uniformModelViewProject, false, 59 | parameters.modelViewProject.get(modelViewProjectData) 60 | ); 61 | 62 | GL20.glUniform1f(uniformScale, parameters.radius * GlyphTexture.RENDER_SCALE); 63 | 64 | return binding; 65 | } 66 | 67 | @Override 68 | public void delete() { 69 | program.delete(); 70 | 71 | MemoryUtil.memFree(modelViewProjectData); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/glyph/GlyphsEffect.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.glyph; 2 | 3 | import dev.gegy.magic.client.casting.drawing.FadingGlyph; 4 | import dev.gegy.magic.client.effect.Effect; 5 | import dev.gegy.magic.client.effect.EffectType; 6 | import dev.gegy.magic.client.glyph.spell.Spell; 7 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 8 | 9 | import java.util.Collection; 10 | 11 | public interface GlyphsEffect extends Effect { 12 | EffectType TYPE = EffectType.create(); 13 | 14 | static GlyphsEffect fromSpell(final Spell spell) { 15 | return homogenous(spell.glyphs(), GlyphRenderParameters::setSpell); 16 | } 17 | 18 | static GlyphsEffect fromFading(final Collection fadingGlyphs) { 19 | return homogenous(fadingGlyphs, GlyphRenderParameters::setFading); 20 | } 21 | 22 | static GlyphsEffect homogenous(final Iterable glyphs, final GlyphRenderParameters.Applicator parametersApplicator) { 23 | return new Homogenous<>(glyphs, parametersApplicator); 24 | } 25 | 26 | void render(GlyphRenderParameters parameters, WorldRenderContext context, RenderFunction render); 27 | 28 | @Override 29 | default EffectType getType() { 30 | return TYPE; 31 | } 32 | 33 | interface RenderFunction { 34 | void accept(GlyphRenderParameters parameters); 35 | } 36 | 37 | record Homogenous( 38 | Iterable glyphs, 39 | GlyphRenderParameters.Applicator parametersApplicator 40 | ) implements GlyphsEffect { 41 | @Override 42 | public void render(final GlyphRenderParameters parameters, final WorldRenderContext context, final RenderFunction render) { 43 | final GlyphRenderParameters.Applicator parametersApplicator = this.parametersApplicator; 44 | for (final T glyph : glyphs) { 45 | parametersApplicator.set(parameters, glyph, context); 46 | render.accept(parameters); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/effect/shader/EffectShader.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.effect.shader; 2 | 3 | import dev.gegy.magic.client.render.gl.GlBinding; 4 | 5 | public interface EffectShader extends AutoCloseable { 6 | GlBinding bind(T parameters); 7 | 8 | void delete(); 9 | 10 | @Override 11 | default void close() { 12 | delete(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/event/ClientRemoveEntityEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.event; 2 | 3 | import net.fabricmc.fabric.api.event.Event; 4 | import net.fabricmc.fabric.api.event.EventFactory; 5 | import net.minecraft.world.entity.Entity; 6 | 7 | public interface ClientRemoveEntityEvent { 8 | Event EVENT = EventFactory.createArrayBacked(ClientRemoveEntityEvent.class, events -> entity -> { 9 | for (ClientRemoveEntityEvent event : events) { 10 | event.onRemoveEntity(entity); 11 | } 12 | }); 13 | 14 | void onRemoveEntity(Entity entity); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/GlyphPlane.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph; 2 | 3 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.joml.Matrix4f; 6 | import org.joml.Vector3f; 7 | 8 | public final class GlyphPlane { 9 | private static final Vector3f UP = new Vector3f(0.0f, 1.0f, 0.0f); 10 | 11 | private final Vector3f origin = new Vector3f(); 12 | private final Vector3f direction = new Vector3f(); 13 | private float distance; 14 | 15 | private final Matrix4f planeToWorld = new Matrix4f(); 16 | private final Matrix4f worldToPlane = new Matrix4f(); 17 | 18 | private final Vector3f vec3 = new Vector3f(); 19 | 20 | public GlyphPlane() { 21 | } 22 | 23 | public GlyphPlane(final Vector3f direction, final float distance) { 24 | set(direction, distance); 25 | } 26 | 27 | public void set(final Vector3f direction, final float distance) { 28 | origin.set(direction).mul(distance); 29 | this.direction.set(direction); 30 | this.distance = distance; 31 | 32 | planeToWorld.rotationTowards(direction, UP).translate(0.0f, 0.0f, distance); 33 | worldToPlane.set(planeToWorld).invert(); 34 | } 35 | 36 | public void set(final GlyphTransform transform, final float tickDelta) { 37 | set(transform.getDirection(tickDelta), transform.getDistance(tickDelta)); 38 | } 39 | 40 | public void set(final GlyphTransform transform) { 41 | set(transform, 1.0f); 42 | } 43 | 44 | @Nullable 45 | public Vector3f raycast(final Vector3f origin, final Vector3f direction) { 46 | final float denominator = direction.dot(this.direction); 47 | if (denominator > 1e-3f) { 48 | final Vector3f delta = this.origin.sub(origin, vec3); 49 | 50 | final float distance = delta.dot(this.direction) / denominator; 51 | if (distance >= 0.0f) { 52 | return direction.mul(distance, vec3).add(origin); 53 | } 54 | } 55 | 56 | return null; 57 | } 58 | 59 | public Vector3f projectToWorld(final Vector3f point) { 60 | return point.mulPosition(planeToWorld); 61 | } 62 | 63 | public Vector3f projectToWorld(final float x, final float y, final float z) { 64 | return projectToWorld(new Vector3f(x, y, z)); 65 | } 66 | 67 | public Vector3f projectToWorld(final float x, final float y) { 68 | return projectToWorld(x, y, 0.0f); 69 | } 70 | 71 | public Vector3f projectFromWorld(final Vector3f point) { 72 | return point.mulPosition(worldToPlane); 73 | } 74 | 75 | public Matrix4f planeToWorld() { 76 | return planeToWorld; 77 | } 78 | 79 | public Vector3f origin() { 80 | return origin; 81 | } 82 | 83 | public Vector3f direction() { 84 | return direction; 85 | } 86 | 87 | public float distance() { 88 | return distance; 89 | } 90 | 91 | public GlyphTransform asTransform() { 92 | return GlyphTransform.of(direction, distance); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/GlyphStroke.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph; 2 | 3 | import java.nio.FloatBuffer; 4 | 5 | public record GlyphStroke(float x0, float y0, float x1, float y1) { 6 | public void writeToBuffer(final FloatBuffer buffer) { 7 | buffer.put(x0).put(y0); 8 | buffer.put(x1).put(y1); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/SpellSource.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph; 2 | 3 | import net.minecraft.world.entity.Entity; 4 | import net.minecraft.world.phys.Vec3; 5 | 6 | public interface SpellSource { 7 | static SpellSource of(final Entity sourceEntity) { 8 | return new SpellSource() { 9 | @Override 10 | public Vec3 getPosition(final float tickDelta) { 11 | return sourceEntity.getEyePosition(tickDelta); 12 | } 13 | 14 | @Override 15 | public Vec3 getLookVector(final float tickDelta) { 16 | return sourceEntity.getViewVector(tickDelta); 17 | } 18 | 19 | @Override 20 | public boolean matchesEntity(final Entity entity) { 21 | return sourceEntity == entity; 22 | } 23 | }; 24 | } 25 | 26 | Vec3 getPosition(float tickDelta); 27 | 28 | Vec3 getLookVector(float tickDelta); 29 | 30 | default boolean matchesEntity(final Entity entity) { 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/Spell.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell; 2 | 3 | import dev.gegy.magic.casting.spell.SpellParameters; 4 | import dev.gegy.magic.client.casting.ClientCastingBuilder; 5 | import dev.gegy.magic.client.casting.blend.CastingBlendType; 6 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 7 | import dev.gegy.magic.client.glyph.SpellSource; 8 | import dev.gegy.magic.client.glyph.spell.transform.SpellTransform; 9 | import dev.gegy.magic.client.glyph.spell.transform.SpellTransformType; 10 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 11 | import dev.gegy.magic.glyph.GlyphForm; 12 | import net.minecraft.world.entity.player.Player; 13 | 14 | import java.util.List; 15 | 16 | public record Spell( 17 | SpellSource source, 18 | SpellTransform transform, 19 | SpellGlyphs glyphs 20 | ) { 21 | public static Spell blendOrCreate(final Player player, final ClientCastingBuilder casting, final SpellParameters parameters, final SpellTransformType transform) { 22 | final Spell spell = casting.blendFrom(CastingBlendType.SPELL, transform); 23 | if (spell != null) { 24 | return spell; 25 | } else { 26 | return create(player, parameters, transform); 27 | } 28 | } 29 | 30 | public static Spell create(final Player player, final SpellParameters parameters, final SpellTransformType transformType) { 31 | final SpellSource source = SpellSource.of(player); 32 | final SpellTransform transform = transformType.create(source, parameters.direction(), parameters.glyphs().size()); 33 | return create(source, transform, parameters.glyphs()); 34 | } 35 | 36 | private static Spell create(final SpellSource source, final SpellTransform transform, final List forms) { 37 | final SpellGlyphs glyphs = new SpellGlyphs(); 38 | for (int i = 0; i < forms.size(); i++) { 39 | final GlyphForm form = forms.get(i); 40 | final GlyphTransform glyphTransform = transform.getTransformForGlyph(i); 41 | glyphs.add(new SpellCastingGlyph(source, form, glyphTransform)); 42 | } 43 | return new Spell(source, transform, glyphs); 44 | } 45 | 46 | public static Spell prepare(final SpellSource source, final SpellTransform transform, final List drawingGlyphs) { 47 | final List forms = drawingGlyphs.stream().map(ClientDrawingGlyph::asForm).toList(); 48 | return create(source, transform, forms); 49 | } 50 | 51 | public void tick() { 52 | transform.tick(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/SpellCasting.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell; 2 | 3 | import dev.gegy.magic.glyph.GlyphType; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.List; 7 | 8 | public final class SpellCasting { 9 | @Nullable 10 | public static GlyphType.CastFunction cast(final List glyphs) { 11 | // TODO: handle spellcasting of combined glyphs 12 | if (glyphs.size() == 1) { 13 | return glyphs.get(0).castFunction(); 14 | } 15 | 16 | return null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/SpellCastingGlyph.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell; 2 | 3 | import dev.gegy.magic.client.glyph.SpellSource; 4 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 5 | import dev.gegy.magic.glyph.GlyphForm; 6 | 7 | public record SpellCastingGlyph( 8 | SpellSource source, 9 | GlyphForm form, 10 | GlyphTransform transform 11 | ) { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/SpellGlyphs.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell; 2 | 3 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 4 | 5 | import java.util.AbstractList; 6 | import java.util.ArrayList; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | 10 | public final class SpellGlyphs extends AbstractList { 11 | private static final float GLYPH_SPACING = 0.2F; 12 | 13 | private final List glyphs = new ArrayList<>(); 14 | 15 | public static float getDistanceForGlyph(final int index) { 16 | return GlyphTransform.DRAW_DISTANCE + index * GLYPH_SPACING; 17 | } 18 | 19 | @Override 20 | public boolean add(final SpellCastingGlyph glyph) { 21 | return glyphs.add(glyph); 22 | } 23 | 24 | @Override 25 | public boolean remove(final Object obj) { 26 | return glyphs.remove(obj); 27 | } 28 | 29 | @Override 30 | public SpellCastingGlyph remove(final int index) { 31 | return glyphs.remove(index); 32 | } 33 | 34 | @Override 35 | public SpellCastingGlyph get(final int index) { 36 | return glyphs.get(index); 37 | } 38 | 39 | @Override 40 | public int size() { 41 | return glyphs.size(); 42 | } 43 | 44 | @Override 45 | public Iterator iterator() { 46 | return glyphs.iterator(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/SpellPrepareBlender.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell; 2 | 3 | import com.google.common.base.Preconditions; 4 | import dev.gegy.magic.client.casting.drawing.ClientDrawingGlyph; 5 | import dev.gegy.magic.client.glyph.transform.BlendingGlyphTransform; 6 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 7 | import dev.gegy.magic.math.AnimationTimer; 8 | 9 | import java.util.List; 10 | 11 | public final class SpellPrepareBlender { 12 | public static final int LENGTH = 6; 13 | 14 | private final Spell spell; 15 | private final AnimationTimer timer; 16 | 17 | private SpellPrepareBlender(final Spell spell, final AnimationTimer timer) { 18 | this.spell = spell; 19 | this.timer = timer; 20 | } 21 | 22 | public static SpellPrepareBlender create(final List drawingGlyphs, final Spell spell) { 23 | Preconditions.checkState(spell.glyphs().size() == drawingGlyphs.size(), "mismatched drawing and prepared glyphs"); 24 | 25 | final SpellGlyphs blendingGlyphs = new SpellGlyphs(); 26 | final Spell blendingSpell = new Spell(spell.source(), spell.transform(), blendingGlyphs); 27 | 28 | final AnimationTimer timer = new AnimationTimer(LENGTH); 29 | 30 | for (int index = 0; index < drawingGlyphs.size(); index++) { 31 | final SpellCastingGlyph glyph = spell.glyphs().get(index); 32 | final ClientDrawingGlyph drawingGlyph = drawingGlyphs.get(index); 33 | 34 | final GlyphTransform sourceTransform = drawingGlyph.plane().asTransform(); 35 | final GlyphTransform targetTransform = spell.transform().getTransformForGlyph(index); 36 | 37 | final BlendingGlyphTransform blendingTransform = new BlendingGlyphTransform(sourceTransform, targetTransform, timer); 38 | blendingGlyphs.add(new SpellCastingGlyph(glyph.source(), glyph.form(), blendingTransform)); 39 | } 40 | 41 | return new SpellPrepareBlender(blendingSpell, timer); 42 | } 43 | 44 | public boolean tick() { 45 | spell.tick(); 46 | return timer.tick(); 47 | } 48 | 49 | public Spell spell() { 50 | return spell; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/transform/SpellTransform.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell.transform; 2 | 3 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 4 | import org.joml.Vector3f; 5 | 6 | public interface SpellTransform extends GlyphTransform { 7 | default void tick() { 8 | } 9 | 10 | @Override 11 | Vector3f getDirection(float tickDelta); 12 | 13 | @Override 14 | float getDistance(float tickDelta); 15 | 16 | GlyphTransform getTransformForGlyph(int index); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/transform/SpellTransformType.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell.transform; 2 | 3 | import dev.gegy.magic.client.glyph.SpellSource; 4 | import dev.gegy.magic.client.glyph.spell.SpellGlyphs; 5 | import org.joml.Vector3f; 6 | 7 | public interface SpellTransformType { 8 | SpellTransformType FIXED = (source, direction, glyphCount) -> { 9 | float castingDistance = SpellGlyphs.getDistanceForGlyph(glyphCount); 10 | return new StaticSpellTransform(direction, castingDistance); 11 | }; 12 | 13 | SpellTransformType TRACKING = (source, direction, glyphCount) -> { 14 | float castingDistance = SpellGlyphs.getDistanceForGlyph(glyphCount); 15 | return new TrackingSpellTransform(source, castingDistance); 16 | }; 17 | 18 | SpellTransform create(SpellSource source, Vector3f direction, int glyphCount); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/transform/StaticSpellTransform.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell.transform; 2 | 3 | import dev.gegy.magic.client.glyph.spell.SpellGlyphs; 4 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 5 | import org.joml.Vector3f; 6 | 7 | public final class StaticSpellTransform implements SpellTransform { 8 | private final Vector3f direction; 9 | private final float distance; 10 | 11 | public StaticSpellTransform(final Vector3f direction, final float distance) { 12 | this.direction = direction; 13 | this.distance = distance; 14 | } 15 | 16 | @Override 17 | public Vector3f getDirection(final float tickDelta) { 18 | return direction; 19 | } 20 | 21 | @Override 22 | public float getDistance(final float tickDelta) { 23 | return distance; 24 | } 25 | 26 | @Override 27 | public GlyphTransform getTransformForGlyph(final int index) { 28 | return GlyphTransform.of(direction, SpellGlyphs.getDistanceForGlyph(index)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/spell/transform/TrackingSpellTransform.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.spell.transform; 2 | 3 | import dev.gegy.magic.client.glyph.SpellSource; 4 | import dev.gegy.magic.client.glyph.spell.SpellGlyphs; 5 | import dev.gegy.magic.client.glyph.transform.GlyphTransform; 6 | import net.minecraft.world.phys.Vec3; 7 | import org.joml.Vector3f; 8 | 9 | public final class TrackingSpellTransform implements SpellTransform { 10 | private final SpellSource source; 11 | 12 | private final Vector3f direction; 13 | private final Vector3f prevDirection; 14 | 15 | private final Vector3f resultDirection = new Vector3f(); 16 | 17 | private final float castingDistance; 18 | 19 | public TrackingSpellTransform(final SpellSource source, final float castingDistance) { 20 | this.source = source; 21 | 22 | direction = source.getLookVector(1.0f).toVector3f(); 23 | prevDirection = new Vector3f(direction); 24 | 25 | this.castingDistance = castingDistance; 26 | } 27 | 28 | @Override 29 | public void tick() { 30 | final Vec3 target = source.getLookVector(1.0f); 31 | 32 | final Vector3f direction = this.direction; 33 | prevDirection.set(direction); 34 | 35 | direction.add( 36 | (float) (target.x - direction.x()) * 0.5f, 37 | (float) (target.y - direction.y()) * 0.5f, 38 | (float) (target.z - direction.z()) * 0.5f 39 | ); 40 | } 41 | 42 | @Override 43 | public Vector3f getDirection(final float tickDelta) { 44 | return prevDirection.lerp(direction, tickDelta, resultDirection); 45 | } 46 | 47 | @Override 48 | public float getDistance(final float tickDelta) { 49 | return castingDistance; 50 | } 51 | 52 | @Override 53 | public GlyphTransform getTransformForGlyph(final int index) { 54 | final float distance = SpellGlyphs.getDistanceForGlyph(index); 55 | 56 | return new GlyphTransform() { 57 | @Override 58 | public Vector3f getDirection(final float tickDelta) { 59 | return TrackingSpellTransform.this.getDirection(tickDelta); 60 | } 61 | 62 | @Override 63 | public float getDistance(final float tickDelta) { 64 | return distance; 65 | } 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/transform/BlendingGlyphTransform.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.transform; 2 | 3 | import dev.gegy.magic.math.AnimationTimer; 4 | import net.minecraft.util.Mth; 5 | import org.joml.Vector3f; 6 | 7 | public final class BlendingGlyphTransform implements GlyphTransform { 8 | private final GlyphTransform source; 9 | private final GlyphTransform target; 10 | private final AnimationTimer timer; 11 | 12 | private final Vector3f direction = new Vector3f(); 13 | 14 | public BlendingGlyphTransform(final GlyphTransform source, final GlyphTransform target, final AnimationTimer timer) { 15 | this.source = source; 16 | this.target = target; 17 | this.timer = timer; 18 | } 19 | 20 | @Override 21 | public Vector3f getDirection(final float tickDelta) { 22 | final float blendProgress = timer.getProgress(tickDelta); 23 | 24 | if (blendProgress <= 0.0f) { 25 | return source.getDirection(tickDelta); 26 | } else if (blendProgress >= 1.0f) { 27 | return target.getDirection(tickDelta); 28 | } 29 | 30 | return source.getDirection(tickDelta).lerp(target.getDirection(tickDelta), blendProgress, direction); 31 | } 32 | 33 | @Override 34 | public float getDistance(final float tickDelta) { 35 | final float blendProgress = timer.getProgress(tickDelta); 36 | 37 | if (blendProgress <= 0.0f) { 38 | return source.getDistance(tickDelta); 39 | } else if (blendProgress >= 1.0f) { 40 | return target.getDistance(tickDelta); 41 | } 42 | 43 | return Mth.lerp(blendProgress, source.getDistance(tickDelta), target.getDistance(tickDelta)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/glyph/transform/GlyphTransform.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.glyph.transform; 2 | 3 | import org.joml.Vector3f; 4 | 5 | public interface GlyphTransform { 6 | float DRAW_DISTANCE = 1.5f; 7 | 8 | static GlyphTransform of(final Vector3f direction, final float distance) { 9 | return new GlyphTransform() { 10 | @Override 11 | public Vector3f getDirection(final float tickDelta) { 12 | return direction; 13 | } 14 | 15 | @Override 16 | public float getDistance(final float tickDelta) { 17 | return distance; 18 | } 19 | }; 20 | } 21 | 22 | Vector3f getDirection(float tickDelta); 23 | 24 | float getDistance(float tickDelta); 25 | 26 | default Vector3f getOrigin(final float tickDelta) { 27 | return new Vector3f(getDirection(tickDelta)).mul(getDistance(tickDelta)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/particle/MagicParticleFactories.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.particle; 2 | 3 | import dev.gegy.magic.particle.MagicParticles; 4 | import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; 5 | 6 | public final class MagicParticleFactories { 7 | public static void register() { 8 | ParticleFactoryRegistry.getInstance().register(MagicParticles.SPARK, SparkParticle.Factory::new); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/GeometryBuilder.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.mojang.blaze3d.vertex.BufferBuilder; 6 | import com.mojang.blaze3d.vertex.BufferVertexConsumer; 7 | import com.mojang.blaze3d.vertex.VertexFormat; 8 | import com.mojang.blaze3d.vertex.VertexFormatElement; 9 | import dev.gegy.magic.client.render.gl.GlGeometry; 10 | 11 | import java.util.function.Consumer; 12 | 13 | public final class GeometryBuilder { 14 | public static final VertexFormatElement POSITION_2F_ELEMENT = new VertexFormatElement(0, VertexFormatElement.Type.FLOAT, VertexFormatElement.Usage.POSITION, 2); 15 | 16 | public static final VertexFormat POSITION_2F = new VertexFormat(ImmutableMap.of("Position", POSITION_2F_ELEMENT)); 17 | 18 | public static GlGeometry uploadQuadPos2f(final float min, final float max) { 19 | return upload(builder -> { 20 | builder.begin(VertexFormat.Mode.QUADS, GeometryBuilder.POSITION_2F); 21 | vertex2f(builder, min, min); 22 | vertex2f(builder, min, max); 23 | vertex2f(builder, max, max); 24 | vertex2f(builder, max, min); 25 | }); 26 | } 27 | 28 | public static GlGeometry upload(final Consumer builderFunction) { 29 | final BufferBuilder builder = new BufferBuilder(64); 30 | builderFunction.accept(builder); 31 | return GlGeometry.upload(builder.end()); 32 | } 33 | 34 | public static void vertex2f(final BufferVertexConsumer builder, final float x, final float y) { 35 | Preconditions.checkState(builder.currentElement() == POSITION_2F_ELEMENT, "invalid element"); 36 | builder.putFloat(0, x); 37 | builder.putFloat(4, y); 38 | builder.nextElement(); 39 | builder.endVertex(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/gl/GlBindableObject.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render.gl; 2 | 3 | public interface GlBindableObject extends GlObject { 4 | GlBinding bind(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/gl/GlBinding.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render.gl; 2 | 3 | public interface GlBinding extends AutoCloseable { 4 | void unbind(); 5 | 6 | @Override 7 | default void close() { 8 | unbind(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/gl/GlBuffer.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render.gl; 2 | 3 | import com.mojang.blaze3d.platform.GlStateManager; 4 | import com.mojang.blaze3d.vertex.BufferUploader; 5 | import org.lwjgl.opengl.GL15; 6 | 7 | import java.nio.ByteBuffer; 8 | 9 | public final class GlBuffer implements GlBindableObject { 10 | private final int id; 11 | 12 | private final int target; 13 | private final int usage; 14 | 15 | private final Binding binding = new Binding(); 16 | 17 | private GlBuffer(final int id, final int target, final int usage) { 18 | this.id = id; 19 | this.target = target; 20 | this.usage = usage; 21 | } 22 | 23 | public static GlBuffer generate(final Target target, final Usage usage) { 24 | final int id = GL15.glGenBuffers(); 25 | return new GlBuffer(id, target.id, usage.id); 26 | } 27 | 28 | @Override 29 | public Binding bind() { 30 | BufferUploader.invalidate(); 31 | GL15.glBindBuffer(target, id); 32 | return binding; 33 | } 34 | 35 | @Override 36 | public void delete() { 37 | GlStateManager._glDeleteBuffers(id); 38 | } 39 | 40 | public final class Binding implements GlBinding { 41 | private Binding() { 42 | } 43 | 44 | public void put(final ByteBuffer data) { 45 | GL15.glBufferData(target, data, usage); 46 | } 47 | 48 | @Override 49 | public void unbind() { 50 | GL15.glBindBuffer(target, 0); 51 | } 52 | } 53 | 54 | public enum Target { 55 | VERTICES(GL15.GL_ARRAY_BUFFER), 56 | INDICES(GL15.GL_ELEMENT_ARRAY_BUFFER); 57 | 58 | private final int id; 59 | 60 | Target(final int id) { 61 | this.id = id; 62 | } 63 | } 64 | 65 | public enum Usage { 66 | STREAM_DRAW(GL15.GL_STREAM_DRAW), 67 | STREAM_READ(GL15.GL_STREAM_READ), 68 | STREAM_COPY(GL15.GL_STREAM_COPY), 69 | STATIC_DRAW(GL15.GL_STATIC_DRAW), 70 | STATIC_READ(GL15.GL_STATIC_READ), 71 | STATIC_COPY(GL15.GL_STATIC_COPY), 72 | DYNAMIC_DRAW(GL15.GL_DYNAMIC_DRAW), 73 | DYNAMIC_READ(GL15.GL_DYNAMIC_READ), 74 | DYNAMIC_COPY(GL15.GL_DYNAMIC_COPY); 75 | 76 | private final int id; 77 | 78 | Usage(final int id) { 79 | this.id = id; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/gl/GlGeometry.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render.gl; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import com.mojang.blaze3d.vertex.BufferBuilder; 5 | 6 | public final class GlGeometry implements GlBindableObject { 7 | private final GlBuffer vertexBuffer; 8 | private final RenderSystem.AutoStorageIndexBuffer indexBuffer; 9 | private final GlVertexArray vertexArray; 10 | 11 | private final int drawMode; 12 | private final int vertexCount; 13 | 14 | private final Binding binding = new Binding(); 15 | 16 | private GlGeometry( 17 | final GlBuffer vertexBuffer, final RenderSystem.AutoStorageIndexBuffer indexBuffer, final GlVertexArray vertexArray, 18 | final int drawMode, final int vertexCount 19 | ) { 20 | this.vertexBuffer = vertexBuffer; 21 | this.indexBuffer = indexBuffer; 22 | this.vertexArray = vertexArray; 23 | this.drawMode = drawMode; 24 | this.vertexCount = vertexCount; 25 | } 26 | 27 | public static GlGeometry upload(final BufferBuilder.RenderedBuffer buffer) { 28 | final BufferBuilder.DrawState parameters = buffer.drawState(); 29 | 30 | final int drawMode = parameters.mode().asGLMode; 31 | final int vertexCount = parameters.indexCount(); 32 | 33 | final GlVertexArray vertexArray = GlVertexArray.generate(); 34 | final GlBuffer vertexBuffer = GlBuffer.generate(GlBuffer.Target.VERTICES, GlBuffer.Usage.STATIC_DRAW); 35 | final RenderSystem.AutoStorageIndexBuffer indexBuffer = RenderSystem.getSequentialBuffer(parameters.mode()); 36 | 37 | try (final GlVertexArray.Binding vertexArrayBinding = vertexArray.bind()) { 38 | final GlBuffer.Binding vertexBufferBinding = vertexBuffer.bind(); 39 | vertexBufferBinding.put(buffer.vertexBuffer()); 40 | indexBuffer.bind(vertexCount); 41 | 42 | vertexArrayBinding.enableFormat(parameters.format()); 43 | 44 | return new GlGeometry(vertexBuffer, indexBuffer, vertexArray, drawMode, vertexCount); 45 | } 46 | } 47 | 48 | @Override 49 | public Binding bind() { 50 | final Binding binding = this.binding; 51 | binding.bind(); 52 | return binding; 53 | } 54 | 55 | @Override 56 | public void delete() { 57 | vertexBuffer.delete(); 58 | vertexArray.delete(); 59 | } 60 | 61 | public final class Binding implements GlBinding { 62 | private GlVertexArray.Binding vertexArrayBinding; 63 | 64 | private Binding() { 65 | } 66 | 67 | private void bind() { 68 | vertexArrayBinding = vertexArray.bind(); 69 | } 70 | 71 | public void draw() { 72 | RenderSystem.drawElements(drawMode, vertexCount, indexBuffer.type().asGLType); 73 | } 74 | 75 | @Override 76 | public void unbind() { 77 | final GlVertexArray.Binding vertexArrayBinding = this.vertexArrayBinding; 78 | if (vertexArrayBinding == null) { 79 | throw new IllegalStateException("cannot unbind geometry - it is already unbound!"); 80 | } 81 | 82 | vertexArrayBinding.unbind(); 83 | this.vertexArrayBinding = null; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/gl/GlObject.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render.gl; 2 | 3 | public interface GlObject extends AutoCloseable { 4 | void delete(); 5 | 6 | @Override 7 | default void close() { 8 | delete(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/client/render/gl/GlVertexArray.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.client.render.gl; 2 | 3 | import com.mojang.blaze3d.platform.GlStateManager; 4 | import com.mojang.blaze3d.vertex.BufferUploader; 5 | import com.mojang.blaze3d.vertex.VertexFormat; 6 | import org.lwjgl.opengl.GL30; 7 | 8 | public final class GlVertexArray implements GlBindableObject { 9 | private final int id; 10 | 11 | private final Binding binding = new Binding(); 12 | 13 | private VertexFormat format; 14 | 15 | private GlVertexArray(final int id) { 16 | this.id = id; 17 | } 18 | 19 | public static GlVertexArray generate() { 20 | final int id = GL30.glGenVertexArrays(); 21 | return new GlVertexArray(id); 22 | } 23 | 24 | @Override 25 | public Binding bind() { 26 | BufferUploader.invalidate(); 27 | GL30.glBindVertexArray(id); 28 | return binding; 29 | } 30 | 31 | @Override 32 | public void delete() { 33 | GlStateManager._glDeleteBuffers(id); 34 | } 35 | 36 | private void enableFormat(final VertexFormat format) { 37 | disableFormat(); 38 | 39 | format.setupBufferState(); 40 | this.format = format; 41 | } 42 | 43 | private void disableFormat() { 44 | final VertexFormat format = this.format; 45 | if (format != null) { 46 | this.format = null; 47 | format.clearBufferState(); 48 | } 49 | } 50 | 51 | public final class Binding implements GlBinding { 52 | private Binding() { 53 | } 54 | 55 | public void enableFormat(final VertexFormat format) { 56 | GlVertexArray.this.enableFormat(format); 57 | } 58 | 59 | public void disableFormat() { 60 | GlVertexArray.this.disableFormat(); 61 | } 62 | 63 | @Override 64 | public void unbind() { 65 | GL30.glBindVertexArray(0); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/event/LateTrackingEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.event; 2 | 3 | import net.fabricmc.fabric.api.event.Event; 4 | import net.fabricmc.fabric.api.event.EventFactory; 5 | import net.minecraft.server.level.ServerPlayer; 6 | import net.minecraft.world.entity.Entity; 7 | 8 | public interface LateTrackingEvent { 9 | Event START = EventFactory.createArrayBacked(LateTrackingEvent.class, events -> (trackedEntity, player) -> { 10 | for (LateTrackingEvent event : events) { 11 | event.onStartTracking(trackedEntity, player); 12 | } 13 | }); 14 | 15 | void onStartTracking(Entity trackedEntity, ServerPlayer player); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/event/PlayerLeaveEvent.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.event; 2 | 3 | import net.fabricmc.fabric.api.event.Event; 4 | import net.fabricmc.fabric.api.event.EventFactory; 5 | import net.minecraft.server.level.ServerPlayer; 6 | 7 | public interface PlayerLeaveEvent { 8 | Event EVENT = EventFactory.createArrayBacked(PlayerLeaveEvent.class, events -> (player) -> { 9 | for (PlayerLeaveEvent event : events) { 10 | event.onPlayerLeave(player); 11 | } 12 | }); 13 | 14 | void onPlayerLeave(ServerPlayer player); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/GlyphForm.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph; 2 | 3 | import dev.gegy.magic.glyph.shape.GlyphShape; 4 | import dev.gegy.magic.network.codec.PacketCodec; 5 | import net.minecraft.network.FriendlyByteBuf; 6 | 7 | public record GlyphForm( 8 | float radius, 9 | GlyphShape shape, 10 | GlyphStyle style 11 | ) { 12 | public static final PacketCodec PACKET_CODEC = PacketCodec.of(GlyphForm::encode, GlyphForm::decode); 13 | 14 | private void encode(final FriendlyByteBuf buf) { 15 | buf.writeFloat(radius); 16 | GlyphShape.PACKET_CODEC.encode(shape, buf); 17 | GlyphStyle.PACKET_CODEC.encode(style, buf); 18 | } 19 | 20 | private static GlyphForm decode(final FriendlyByteBuf buf) { 21 | final float radius = buf.readFloat(); 22 | final GlyphShape shape = GlyphShape.PACKET_CODEC.decode(buf); 23 | final GlyphStyle style = GlyphStyle.PACKET_CODEC.decode(buf); 24 | return new GlyphForm(radius, shape, style); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/GlyphStyle.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph; 2 | 3 | import dev.gegy.magic.math.ColorHsluv; 4 | import dev.gegy.magic.math.ColorRgb; 5 | import dev.gegy.magic.network.codec.PacketCodec; 6 | import net.minecraft.network.FriendlyByteBuf; 7 | 8 | public record GlyphStyle(ColorRgb primaryColor, ColorRgb secondaryColor) { 9 | public static final PacketCodec PACKET_CODEC = PacketCodec.of(GlyphStyle::encode, GlyphStyle::decode); 10 | 11 | public static final GlyphStyle WILD = new GlyphStyle(ColorRgb.of(0x364684), ColorRgb.of(0xD3DFE5)); 12 | 13 | public static final GlyphStyle RED = GlyphStyle.of(10.0f / 360.0f); 14 | public static final GlyphStyle PURPLE = GlyphStyle.of(280.0f / 360.0f); 15 | 16 | public static GlyphStyle of(final float hue) { 17 | final ColorHsluv primary = ColorHsluv.of(hue, 0.85f, 0.45f); 18 | final ColorHsluv secondary = ColorHsluv.of(ColorHsluv.warmHue(hue, 30.0f / 360.0f), 0.6f, 0.85f); 19 | return new GlyphStyle(primary.toRgb(), secondary.toRgb()); 20 | } 21 | 22 | private void encode(final FriendlyByteBuf buf) { 23 | ColorRgb.PACKET_CODEC.encode(primaryColor, buf); 24 | ColorRgb.PACKET_CODEC.encode(secondaryColor, buf); 25 | } 26 | 27 | private static GlyphStyle decode(final FriendlyByteBuf buf) { 28 | final ColorRgb primaryColor = ColorRgb.PACKET_CODEC.decode(buf); 29 | final ColorRgb secondaryColor = ColorRgb.PACKET_CODEC.decode(buf); 30 | return new GlyphStyle(primaryColor, secondaryColor); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/GlyphType.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.ServerCasting; 5 | import dev.gegy.magic.casting.ServerCastingBuilder; 6 | import dev.gegy.magic.casting.spell.SpellParameters; 7 | import dev.gegy.magic.casting.spell.beam.ServerCastingBeam; 8 | import dev.gegy.magic.casting.spell.teleport.ServerCastingTeleport; 9 | import dev.gegy.magic.network.codec.PacketCodec; 10 | import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder; 11 | import net.fabricmc.fabric.api.event.registry.RegistryAttribute; 12 | import net.minecraft.core.MappedRegistry; 13 | import net.minecraft.core.Registry; 14 | import net.minecraft.resources.ResourceKey; 15 | import net.minecraft.server.level.ServerPlayer; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.util.Objects; 19 | 20 | public final class GlyphType { 21 | public static final MappedRegistry REGISTRY = FabricRegistryBuilder.createSimple(ResourceKey.createRegistryKey(Magic.identifier("glyph_type"))) 22 | .attribute(RegistryAttribute.SYNCED) 23 | .buildAndRegister(); 24 | 25 | public static final PacketCodec<@Nullable GlyphType> PACKET_CODEC = PacketCodec.ofRegistry(REGISTRY); 26 | 27 | public static final GlyphType BEAM = register("beam", GlyphType.builder() 28 | .style(GlyphStyle.RED) 29 | .casts(ServerCastingBeam::build) 30 | ); 31 | public static final GlyphType TELEPORT = register("teleport", GlyphType.builder() 32 | .style(GlyphStyle.PURPLE) 33 | .casts(ServerCastingTeleport::build) 34 | ); 35 | 36 | private final GlyphStyle style; 37 | private final CastFunction castFunction; 38 | 39 | private GlyphType(final GlyphStyle style, final CastFunction castFunction) { 40 | this.style = style; 41 | this.castFunction = castFunction; 42 | } 43 | 44 | public static void onInitialize() { 45 | } 46 | 47 | public static Builder builder() { 48 | return new Builder(); 49 | } 50 | 51 | private static GlyphType register(final String id, final Builder glyph) { 52 | return Registry.register(REGISTRY, Magic.identifier(id), glyph.build()); 53 | } 54 | 55 | public GlyphStyle style() { 56 | return style; 57 | } 58 | 59 | public CastFunction castFunction() { 60 | return castFunction; 61 | } 62 | 63 | public static final class Builder { 64 | private GlyphStyle style; 65 | private CastFunction castFunction; 66 | 67 | private Builder() { 68 | } 69 | 70 | public Builder style(final GlyphStyle style) { 71 | this.style = style; 72 | return this; 73 | } 74 | 75 | public Builder casts(final CastFunction castFunction) { 76 | this.castFunction = castFunction; 77 | return this; 78 | } 79 | 80 | public GlyphType build() { 81 | return new GlyphType( 82 | Objects.requireNonNull(style, "style not set"), 83 | Objects.requireNonNull(castFunction, "casting build function not set") 84 | ); 85 | } 86 | } 87 | 88 | public interface CastFunction { 89 | ServerCasting build(ServerPlayer player, SpellParameters spell, ServerCastingBuilder casting); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/shape/GlyphDebugRenderer.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph.shape; 2 | 3 | import it.unimi.dsi.fastutil.bytes.Byte2ObjectMap; 4 | import it.unimi.dsi.fastutil.bytes.Byte2ObjectOpenHashMap; 5 | import net.minecraft.world.phys.Vec2; 6 | 7 | import javax.imageio.ImageIO; 8 | import java.awt.Color; 9 | import java.awt.Graphics2D; 10 | import java.awt.image.BufferedImage; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Random; 16 | 17 | public final class GlyphDebugRenderer { 18 | private static final int IMAGE_SIZE = 64; 19 | private static final int IMAGE_PADDING = 4; 20 | 21 | private static final int GLYPH_DIAMETER = IMAGE_SIZE - IMAGE_PADDING * 2; 22 | private static final int GLYPH_RADIUS = GLYPH_DIAMETER / 2; 23 | 24 | private static final int MIN_SIZE = 3; 25 | private static final int MAX_SIZE = 6; 26 | 27 | private static final int RENDER_PER_SIZE = 10; 28 | 29 | public static void main(final String[] args) throws IOException { 30 | final GlyphShapeGenerator generator = new GlyphShapeGenerator(MIN_SIZE, MAX_SIZE); 31 | final List all = generator.generateAll(); 32 | 33 | final Byte2ObjectMap> bySize = new Byte2ObjectOpenHashMap<>(); 34 | for (final GlyphShape glyph : all) { 35 | bySize.computeIfAbsent((byte) glyph.size(), s -> new ArrayList<>()).add(glyph); 36 | } 37 | 38 | final Random random = new Random(); 39 | for (int size = MIN_SIZE; size <= MAX_SIZE; size++) { 40 | final List glyphsBySize = bySize.get((byte) size); 41 | if (glyphsBySize == null || glyphsBySize.isEmpty()) { 42 | continue; 43 | } 44 | 45 | for (int i = 0; i < RENDER_PER_SIZE; i++) { 46 | if (glyphsBySize.isEmpty()) { 47 | break; 48 | } 49 | 50 | final GlyphShape glyph = glyphsBySize.remove(random.nextInt(glyphsBySize.size())); 51 | 52 | final BufferedImage image = render(glyph); 53 | ImageIO.write(image, "png", new File("glyph_" + size + "_" + i + ".png")); 54 | } 55 | } 56 | } 57 | 58 | public static BufferedImage render(final GlyphShape glyph) { 59 | final BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB); 60 | final Graphics2D graphics = image.createGraphics(); 61 | 62 | graphics.setColor(Color.RED); 63 | graphics.drawOval(IMAGE_PADDING, IMAGE_PADDING, GLYPH_DIAMETER, GLYPH_DIAMETER); 64 | 65 | graphics.setColor(Color.BLUE); 66 | for (final GlyphEdge edge : glyph) { 67 | final Vec2 from = edge.from.getPoint(); 68 | final Vec2 to = edge.to.getPoint(); 69 | 70 | graphics.drawLine( 71 | transformCoordinate(from.x), transformCoordinate(-from.y), 72 | transformCoordinate(to.x), transformCoordinate(-to.y) 73 | ); 74 | 75 | if (from.x != 0.0 || to.x != 0.0) { 76 | graphics.drawLine( 77 | transformCoordinate(-from.x), transformCoordinate(-from.y), 78 | transformCoordinate(-to.x), transformCoordinate(-to.y) 79 | ); 80 | } 81 | } 82 | 83 | return image; 84 | } 85 | 86 | private static int transformCoordinate(final float coordinate) { 87 | return Math.round((coordinate + 1.0f) * GLYPH_RADIUS) + IMAGE_PADDING; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/shape/GlyphNode.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph.shape; 2 | 3 | import dev.gegy.magic.network.codec.PacketCodec; 4 | import net.minecraft.world.phys.Vec2; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public enum GlyphNode { 11 | TOP(true, 0.0f, 1.0f), 12 | BOTTOM(true, 0.0f, -1.0f), 13 | SIDE_UPPER(true, (float) Math.sqrt(0.75f), 0.5f), 14 | SIDE_LOWER(true, (float) Math.sqrt(0.75f), -0.5f), 15 | CENTER_UPPER(false, 0.0f, 0.5f), 16 | CENTER(false, 0.0f, 0.0f), 17 | CENTER_LOWER(false, 0.0f, -0.5f); 18 | 19 | public static final PacketCodec<@Nullable GlyphNode> PACKET_CODEC = PacketCodec.of( 20 | (node, buf) -> { 21 | int id = node != null ? node.ordinal() : 0xFF; 22 | buf.writeByte(id & 0xFF); 23 | }, 24 | buf -> GlyphNode.byId(buf.readUnsignedByte()) 25 | ); 26 | 27 | public static final GlyphNode[] NODES = values(); 28 | 29 | public static final GlyphNode[] CIRCUMFERENCE; 30 | 31 | static { 32 | final List circumference = new ArrayList<>(); 33 | for (final GlyphNode node : NODES) { 34 | if (node.circumference) { 35 | circumference.add(node); 36 | } 37 | } 38 | 39 | CIRCUMFERENCE = circumference.toArray(new GlyphNode[0]); 40 | } 41 | 42 | private final boolean circumference; 43 | private final Vec2 point; 44 | 45 | GlyphNode(final boolean circumference, final float x, final float y) { 46 | this.circumference = circumference; 47 | point = new Vec2(x, y); 48 | } 49 | 50 | public boolean isAtCircumference() { 51 | return circumference; 52 | } 53 | 54 | public Vec2 getPoint() { 55 | return point; 56 | } 57 | 58 | @Nullable 59 | public static GlyphNode byId(final short nodeId) { 60 | if (nodeId >= 0 && nodeId < GlyphNode.NODES.length) { 61 | return GlyphNode.NODES[nodeId]; 62 | } 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/shape/GlyphShape.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph.shape; 2 | 3 | import com.google.common.collect.Iterators; 4 | import dev.gegy.magic.network.codec.PacketCodec; 5 | 6 | import java.util.Arrays; 7 | import java.util.Iterator; 8 | 9 | public record GlyphShape(int mask) implements Iterable { 10 | private static final int ALL_EDGES = toMask(GlyphEdge.EDGES); 11 | private static final int CENTER_LINE = toMask(GlyphEdge.CENTER_LINE); 12 | 13 | public static final GlyphShape EMPTY = new GlyphShape(0); 14 | 15 | public static final PacketCodec PACKET_CODEC = PacketCodec.of( 16 | (shape, buf) -> buf.writeShort(shape.mask), 17 | buf -> new GlyphShape(buf.readShort()) 18 | ); 19 | 20 | public GlyphShape { 21 | mask &= ALL_EDGES; 22 | } 23 | 24 | public boolean contains(final GlyphEdge edge) { 25 | return (mask & edge.mask()) != 0; 26 | } 27 | 28 | public GlyphShape withEdge(final GlyphEdge edge) { 29 | return new GlyphShape(mask | edge.mask()); 30 | } 31 | 32 | public int size() { 33 | final int edgeCount = Integer.bitCount(mask); 34 | if (!canSimplify(mask)) { 35 | return edgeCount; 36 | } 37 | 38 | // all the points along the center are colinear, and to give an accurate measurement of the "size" of a glyph, 39 | // the merged line across is more meaningful. 40 | int simplifiedSize = edgeCount; 41 | int mergedLength = 0; 42 | 43 | for (final GlyphEdge edge : GlyphEdge.CENTER_LINE) { 44 | if (contains(edge)) { 45 | mergedLength++; 46 | simplifiedSize--; 47 | } else { 48 | if (mergedLength > 0) { 49 | mergedLength = 0; 50 | simplifiedSize++; 51 | } 52 | } 53 | } 54 | 55 | if (mergedLength > 0) { 56 | simplifiedSize++; 57 | } 58 | 59 | return simplifiedSize; 60 | } 61 | 62 | private static boolean canSimplify(final int glyph) { 63 | // we can potentially simplify if we have 2+ edges in the center 64 | final int edgesInCenterLine = glyph & CENTER_LINE; 65 | return Integer.bitCount(edgesInCenterLine) >= 2; 66 | } 67 | 68 | @Override 69 | public Iterator iterator() { 70 | return Iterators.filter(Iterators.forArray(GlyphEdge.EDGES), this::contains); 71 | } 72 | 73 | private static int toMask(final GlyphEdge[] edges) { 74 | return Arrays.stream(edges).mapToInt(GlyphEdge::mask).reduce(0, (a, b) -> a | b); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/glyph/shape/GlyphShapeGenerator.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.glyph.shape; 2 | 3 | import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.function.Consumer; 9 | 10 | public final class GlyphShapeGenerator { 11 | private final int minSize; 12 | private final int maxSize; 13 | 14 | public GlyphShapeGenerator(final int minSize, final int maxSize) { 15 | this.minSize = minSize; 16 | this.maxSize = maxSize; 17 | } 18 | 19 | public List generateAll() { 20 | // 1. pick a point 21 | // 2. pick another point connected to the current point and construct an edge 22 | // 3. if this new point is on the circumference, we can either: 23 | // - continue our current path and return to step 2 24 | // - start from a newly picked point 25 | // when picking a new point, we CAN pick a point that has already been picked 26 | // but, we CANNOT pick an edge that has already been constructed 27 | 28 | final Set result = new ObjectOpenHashSet<>(); 29 | generateAll(result::add); 30 | return new ArrayList<>(result); 31 | } 32 | 33 | private void generateAll(final Consumer add) { 34 | // we always have to start from a point on the circumference because that is where our pen must start! 35 | for (final GlyphNode rootNode : GlyphNode.CIRCUMFERENCE) { 36 | advanceFromRoot(GlyphShape.EMPTY, rootNode, add); 37 | } 38 | } 39 | 40 | private void advanceFromRoot(final GlyphShape glyph, final GlyphNode node, final Consumer add) { 41 | for (final GlyphEdge nextEdge : GlyphEdge.getConnectedEdgesTo(node)) { 42 | tryAdvanceWithEdge(glyph, node, nextEdge, add); 43 | } 44 | } 45 | 46 | private void tryAdvanceFrom(final GlyphShape glyph, final GlyphNode node, final Consumer add) { 47 | final int size = glyph.size(); 48 | 49 | // if this glyph has enough edges, yield 50 | if (size >= minSize) { 51 | add.accept(glyph); 52 | } 53 | 54 | // only continue picking new points if this glyph has not exceeded the maximum size 55 | if (size >= maxSize) { 56 | return; 57 | } 58 | 59 | // if this node is on the circumference, we can "pick up our pencil" to jump to a new node on the circumference 60 | if (node.isAtCircumference()) { 61 | for (final GlyphNode nextNode : GlyphNode.CIRCUMFERENCE) { 62 | if (nextNode != node) { 63 | advanceFromRoot(glyph, nextNode, add); 64 | } 65 | } 66 | } 67 | 68 | // find the all next nodes to travel to 69 | for (final GlyphEdge nextEdge : GlyphEdge.getConnectedEdgesTo(node)) { 70 | tryAdvanceWithEdge(glyph, node, nextEdge, add); 71 | } 72 | } 73 | 74 | private void tryAdvanceWithEdge(final GlyphShape glyph, final GlyphNode node, final GlyphEdge nextEdge, final Consumer add) { 75 | if (!glyph.contains(nextEdge)) { 76 | final GlyphShape nextGlyph = glyph.withEdge(nextEdge); 77 | final GlyphNode nextNode = nextEdge.getOther(node); 78 | tryAdvanceFrom(nextGlyph, nextNode, add); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/math/AnimatedColor.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.math; 2 | 3 | import net.minecraft.util.Mth; 4 | 5 | public final class AnimatedColor { 6 | private float red, green, blue; 7 | private float prevRed, prevGreen, prevBlue; 8 | private float targetRed, targetGreen, targetBlue; 9 | 10 | public AnimatedColor(final ColorRgb color) { 11 | red = targetRed = prevRed = color.red(); 12 | green = targetGreen = prevGreen = color.green(); 13 | blue = targetBlue = prevBlue = color.blue(); 14 | } 15 | 16 | public void tick(final float lerpSpeed) { 17 | prevRed = red; 18 | prevGreen = green; 19 | prevBlue = blue; 20 | 21 | red += (targetRed - red) * lerpSpeed; 22 | green += (targetGreen - green) * lerpSpeed; 23 | blue += (targetBlue - blue) * lerpSpeed; 24 | } 25 | 26 | public void set(final ColorRgb color) { 27 | targetRed = color.red(); 28 | targetGreen = color.green(); 29 | targetBlue = color.blue(); 30 | } 31 | 32 | public float getRed(final float tickDelta) { 33 | return Mth.lerp(tickDelta, prevRed, red); 34 | } 35 | 36 | public float getGreen(final float tickDelta) { 37 | return Mth.lerp(tickDelta, prevGreen, green); 38 | } 39 | 40 | public float getBlue(final float tickDelta) { 41 | return Mth.lerp(tickDelta, prevBlue, blue); 42 | } 43 | 44 | public ColorRgb get(final float tickDelta) { 45 | return ColorRgb.of(getRed(tickDelta), getGreen(tickDelta), getBlue(tickDelta)); 46 | } 47 | 48 | public ColorRgb target() { 49 | return ColorRgb.of(targetRed, targetGreen, targetBlue); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/math/AnimationTimer.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.math; 2 | 3 | public final class AnimationTimer { 4 | private final int length; 5 | private int time; 6 | 7 | public AnimationTimer(final int length) { 8 | this.length = length; 9 | } 10 | 11 | public boolean tick() { 12 | return time++ > length; 13 | } 14 | 15 | public float getProgress(final float tickDelta) { 16 | final float time = Math.min(this.time + tickDelta, length); 17 | return time / length; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/math/ColorHsluv.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.math; 2 | 3 | import net.minecraft.util.Mth; 4 | import org.hsluv.HUSLColorConverter; 5 | 6 | public record ColorHsluv(float hue, float saturation, float light) { 7 | public static ColorHsluv of(final float hue, final float saturation, final float light) { 8 | return new ColorHsluv(hue, saturation, light); 9 | } 10 | 11 | static ColorHsluv fromTuple(final double[] tuple) { 12 | return new ColorHsluv((float) (tuple[0] / 360.0), (float) (tuple[1] / 100.0), (float) (tuple[2] / 100.0)); 13 | } 14 | 15 | public ColorHsluv warmHue(final float amount) { 16 | return withHue(warmHue(hue, amount)); 17 | } 18 | 19 | public ColorHsluv coolHue(final float amount) { 20 | return warmHue(-amount); 21 | } 22 | 23 | public ColorHsluv mulSaturation(final float factor) { 24 | final float saturation = Mth.clamp(this.saturation * factor, 0.0f, 1.0f); 25 | return withSaturation(saturation); 26 | } 27 | 28 | public ColorHsluv mulLight(final float factor) { 29 | final float light = Mth.clamp(this.light * factor, 0.0f, 1.0f); 30 | return withLight(light); 31 | } 32 | 33 | public ColorHsluv withHue(final float hue) { 34 | return new ColorHsluv(hue, saturation, light); 35 | } 36 | 37 | public ColorHsluv withSaturation(final float saturation) { 38 | return new ColorHsluv(hue, saturation, light); 39 | } 40 | 41 | public ColorHsluv withLight(final float light) { 42 | return new ColorHsluv(hue, saturation, light); 43 | } 44 | 45 | public ColorRgb toRgb() { 46 | return ColorRgb.fromTuple(HUSLColorConverter.hsluvToRgb(toTuple())); 47 | } 48 | 49 | double[] toTuple() { 50 | return new double[] { hue * 360.0, saturation * 100.0, light * 100.0 }; 51 | } 52 | 53 | public static float warmHue(float hue, final float amount) { 54 | if (hue < amount || hue > 0.65f) { 55 | hue += amount; 56 | } else { 57 | hue -= amount; 58 | } 59 | 60 | if (hue > 1.0f) hue -= 1.0f; 61 | if (hue < 0.0f) hue += 1.0f; 62 | 63 | return hue; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/math/ColorRgb.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.math; 2 | 3 | import dev.gegy.magic.network.codec.PacketCodec; 4 | import net.minecraft.util.Mth; 5 | import org.hsluv.HUSLColorConverter; 6 | 7 | public final class ColorRgb { 8 | public static final PacketCodec PACKET_CODEC = PacketCodec.of( 9 | (color, buf) -> buf.writeInt(color.packed()), 10 | buf -> ColorRgb.of(buf.readInt()) 11 | ); 12 | 13 | public static final ColorRgb WHITE = ColorRgb.of(1.0f, 1.0f, 1.0f); 14 | 15 | private final float red; 16 | private final float green; 17 | private final float blue; 18 | private final int packed; 19 | 20 | private ColorRgb(final float red, final float green, final float blue, final int packed) { 21 | this.red = red; 22 | this.green = green; 23 | this.blue = blue; 24 | this.packed = packed; 25 | } 26 | 27 | public static ColorRgb of(final float red, final float green, final float blue) { 28 | final int redValue = Mth.floor(red * 255.0f) & 0xFF; 29 | final int greenValue = Mth.floor(green * 255.0f) & 0xFF; 30 | final int blueValue = Mth.floor(blue * 255.0f) & 0xFF; 31 | final int packed = redValue << 16 | greenValue << 8 | blueValue; 32 | 33 | return new ColorRgb(red, green, blue, packed); 34 | } 35 | 36 | public static ColorRgb of(final int packed) { 37 | final float red = (packed >> 16 & 0xFF) / 255.0f; 38 | final float green = (packed >> 8 & 0xFF) / 255.0f; 39 | final float blue = (packed & 0xFF) / 255.0f; 40 | return new ColorRgb(red, green, blue, packed); 41 | } 42 | 43 | static ColorRgb fromTuple(final double[] tuple) { 44 | return ColorRgb.of((float) tuple[0], (float) tuple[1], (float) tuple[2]); 45 | } 46 | 47 | public float red() { 48 | return red; 49 | } 50 | 51 | public float green() { 52 | return green; 53 | } 54 | 55 | public float blue() { 56 | return blue; 57 | } 58 | 59 | public int packed() { 60 | return packed; 61 | } 62 | 63 | public ColorHsluv toHsluv() { 64 | return ColorHsluv.fromTuple(HUSLColorConverter.rgbToHsluv(toTuple())); 65 | } 66 | 67 | double[] toTuple() { 68 | return new double[] { red, green, blue }; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return Integer.toHexString(packed); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/math/Easings.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.math; 2 | 3 | // Easing functions adapted from . 4 | public final class Easings { 5 | public static float easeInCirc(final float x) { 6 | return (float) (1.0f - Math.sqrt(1.0f - x * x)); 7 | } 8 | 9 | public static float easeOutCirc(final float x) { 10 | return (float) Math.sqrt(2.0f * x - x * x); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/mixin/ServerEntityMixin.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.mixin; 2 | 3 | import dev.gegy.magic.event.LateTrackingEvent; 4 | import net.minecraft.server.level.ServerEntity; 5 | import net.minecraft.server.level.ServerPlayer; 6 | import net.minecraft.world.entity.Entity; 7 | import org.spongepowered.asm.mixin.Final; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | @Mixin(ServerEntity.class) 15 | public class ServerEntityMixin { 16 | @Shadow 17 | @Final 18 | private Entity entity; 19 | 20 | @Inject(method = "addPairing", at = @At("TAIL")) 21 | private void onStartTracking(final ServerPlayer player, final CallbackInfo ci) { 22 | LateTrackingEvent.START.invoker().onStartTracking(entity, player); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/mixin/client/ClientEntityCallbacksMixin.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.mixin.client; 2 | 3 | import dev.gegy.magic.client.event.ClientRemoveEntityEvent; 4 | import net.minecraft.world.entity.Entity; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | @Mixin(targets = "net/minecraft/client/multiplayer/ClientLevel$EntityCallbacks") 11 | public class ClientEntityCallbacksMixin { 12 | @Inject(method = "onDestroyed(Lnet/minecraft/world/entity/Entity;)V", at = @At("HEAD")) 13 | private void destroy(final Entity entity, final CallbackInfo ci) { 14 | ClientRemoveEntityEvent.EVENT.invoker().onRemoveEntity(entity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/mixin/client/PlayerMixin.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.mixin.client; 2 | 3 | import dev.gegy.magic.client.animator.CastingAnimatableEntity; 4 | import dev.gegy.magic.client.animator.CastingAnimator; 5 | import net.minecraft.world.entity.EntityType; 6 | import net.minecraft.world.entity.LivingEntity; 7 | import net.minecraft.world.entity.player.Player; 8 | import net.minecraft.world.level.Level; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Unique; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(Player.class) 16 | public abstract class PlayerMixin extends LivingEntity implements CastingAnimatableEntity { 17 | @Unique 18 | private final CastingAnimator castingAnimator = new CastingAnimator(); 19 | 20 | private PlayerMixin(final EntityType type, final Level level) { 21 | super(type, level); 22 | } 23 | 24 | @Override 25 | public CastingAnimator getCastingAnimator() { 26 | return castingAnimator; 27 | } 28 | 29 | @Inject(method = "tick", at = @At("TAIL")) 30 | private void tick(final CallbackInfo ci) { 31 | if (level().isClientSide) { 32 | castingAnimator.tick((Player) (Object) this); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/mixin/client/PlayerModelMixin.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.mixin.client; 2 | 3 | import dev.gegy.magic.client.animator.CastingAnimatableEntity; 4 | import dev.gegy.magic.client.animator.CastingAnimator; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.model.HumanoidModel; 7 | import net.minecraft.client.model.PlayerModel; 8 | import net.minecraft.client.model.geom.ModelPart; 9 | import net.minecraft.world.entity.Entity; 10 | import net.minecraft.world.entity.LivingEntity; 11 | import net.minecraft.world.entity.player.Player; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | 17 | @Mixin(PlayerModel.class) 18 | public abstract class PlayerModelMixin extends HumanoidModel { 19 | private PlayerModelMixin(final ModelPart root) { 20 | super(root); 21 | } 22 | 23 | @Inject( 24 | method = "setupAnim(Lnet/minecraft/world/entity/LivingEntity;FFFFF)V", 25 | at = @At( 26 | value = "INVOKE", 27 | target = "Lnet/minecraft/client/model/HumanoidModel;setupAnim(Lnet/minecraft/world/entity/LivingEntity;FFFFF)V", 28 | shift = At.Shift.AFTER 29 | ) 30 | ) 31 | private void setAngles(final T entity, final float limbAngle, final float limbDistance, final float animationProgress, final float headYaw, final float headPitch, final CallbackInfo ci) { 32 | final Minecraft client = Minecraft.getInstance(); 33 | final Entity cameraEntity = client.cameraEntity != null ? client.cameraEntity : client.player; 34 | if (cameraEntity == entity && client.options.getCameraType().isFirstPerson()) { 35 | return; 36 | } 37 | 38 | if (entity instanceof final CastingAnimatableEntity animatable) { 39 | final CastingAnimator animator = animatable.getCastingAnimator(); 40 | animator.applyToModel((Player) entity, leftArm, rightArm, client.getFrameTime()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/NetworkAddressing.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network; 2 | 3 | import net.fabricmc.fabric.api.networking.v1.PlayerLookup; 4 | import net.minecraft.server.level.ServerPlayer; 5 | import net.minecraft.util.Unit; 6 | 7 | import java.util.function.BiConsumer; 8 | import java.util.function.Consumer; 9 | 10 | public interface NetworkAddressing { 11 | static NetworkAddressing server() { 12 | return new NetworkAddressing<>() { 13 | @Override 14 | public void send(final Consumer handler) { 15 | handler.accept(Unit.INSTANCE); 16 | } 17 | 18 | @Override 19 | public void broadcast(final Consumer handler) { 20 | } 21 | }; 22 | } 23 | 24 | static NetworkAddressing trackingClients(final ServerPlayer player) { 25 | return new NetworkAddressing<>() { 26 | @Override 27 | public void send(final Consumer handler) { 28 | handler.accept(player); 29 | } 30 | 31 | @Override 32 | public void broadcast(final Consumer handler) { 33 | for (final ServerPlayer tracking : PlayerLookup.tracking(player)) { 34 | handler.accept(tracking); 35 | } 36 | } 37 | }; 38 | } 39 | 40 | void send(Consumer handler); 41 | 42 | void broadcast(Consumer handler); 43 | 44 | default void broadcastAndSend(final Consumer handler) { 45 | broadcast(handler); 46 | send(handler); 47 | } 48 | 49 | default

NetworkSender

sender(final BiConsumer sender) { 50 | return NetworkSender.of(this, sender); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/NetworkSender.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network; 2 | 3 | import java.util.function.BiConsumer; 4 | import java.util.function.Function; 5 | 6 | public interface NetworkSender { 7 | static NetworkSender

of(final NetworkAddressing addressing, final BiConsumer sender) { 8 | return new NetworkSender<>() { 9 | @Override 10 | public void send(final P value) { 11 | addressing.send(target -> sender.accept(target, value)); 12 | } 13 | 14 | @Override 15 | public void broadcast(final P value) { 16 | addressing.broadcast(target -> sender.accept(target, value)); 17 | } 18 | 19 | @Override 20 | public void broadcastAndSend(final P value) { 21 | addressing.broadcastAndSend(target -> sender.accept(target, value)); 22 | } 23 | }; 24 | } 25 | 26 | void send(T value); 27 | 28 | void broadcast(T value); 29 | 30 | default void broadcastAndSend(final T value) { 31 | broadcast(value); 32 | send(value); 33 | } 34 | 35 | default NetworkSender map(final Function function) { 36 | return new NetworkSender<>() { 37 | @Override 38 | public void send(final U value) { 39 | NetworkSender.this.send(function.apply(value)); 40 | } 41 | 42 | @Override 43 | public void broadcast(final U value) { 44 | NetworkSender.this.broadcast(function.apply(value)); 45 | } 46 | 47 | @Override 48 | public void broadcastAndSend(final U value) { 49 | NetworkSender.this.broadcastAndSend(function.apply(value)); 50 | } 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/c2s/CastingEventC2SPacket.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.c2s; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 6 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 7 | import net.minecraft.network.FriendlyByteBuf; 8 | import net.minecraft.resources.ResourceLocation; 9 | 10 | public final class CastingEventC2SPacket { 11 | static final ResourceLocation CHANNEL = Magic.identifier("casting_event"); 12 | 13 | public static void sendToServer(final CastingEventSpec spec, final T event) { 14 | final FriendlyByteBuf buf = PacketByteBufs.create(); 15 | buf.writeResourceLocation(spec.id()); 16 | spec.codec().encode(event, buf); 17 | 18 | ClientPlayNetworking.send(CHANNEL, buf); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/c2s/MagicC2SNetworking.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.c2s; 2 | 3 | import dev.gegy.magic.casting.ServerCastingTracker; 4 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 5 | import net.minecraft.resources.ResourceLocation; 6 | 7 | public final class MagicC2SNetworking { 8 | public static void registerReceivers() { 9 | ServerPlayNetworking.registerGlobalReceiver(CastingEventC2SPacket.CHANNEL, (server, player, handler, buf, responseSender) -> { 10 | final ResourceLocation id = buf.readResourceLocation(); 11 | buf.retain(); 12 | 13 | server.submit(() -> { 14 | try { 15 | ServerCastingTracker.INSTANCE.handleEvent(player, id, buf); 16 | } finally { 17 | buf.release(); 18 | } 19 | }); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/codec/PacketDecoder.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.codec; 2 | 3 | import net.minecraft.network.FriendlyByteBuf; 4 | 5 | public interface PacketDecoder { 6 | T decode(FriendlyByteBuf buf); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/codec/PacketEncoder.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.codec; 2 | 3 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 4 | import net.minecraft.network.FriendlyByteBuf; 5 | 6 | public interface PacketEncoder { 7 | void encode(T value, FriendlyByteBuf buf); 8 | 9 | default FriendlyByteBuf encodeStart(final T value) { 10 | final FriendlyByteBuf buf = PacketByteBufs.create(); 11 | encode(value, buf); 12 | return buf; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/s2c/CastingEventS2CPacket.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.s2c; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.casting.event.CastingEventSpec; 5 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 6 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 7 | import net.minecraft.network.FriendlyByteBuf; 8 | import net.minecraft.resources.ResourceLocation; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.minecraft.world.entity.player.Player; 11 | 12 | public final class CastingEventS2CPacket { 13 | static final ResourceLocation CHANNEL = Magic.identifier("casting_event"); 14 | 15 | public static FriendlyByteBuf create(final Player source, final CastingEventSpec spec, final T event) { 16 | final FriendlyByteBuf buf = PacketByteBufs.create(); 17 | buf.writeVarInt(source.getId()); 18 | buf.writeResourceLocation(spec.id()); 19 | spec.codec().encode(event, buf); 20 | return buf; 21 | } 22 | 23 | public static void sendTo(final ServerPlayer player, final FriendlyByteBuf buf) { 24 | ServerPlayNetworking.send(player, CHANNEL, buf); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/s2c/MagicS2CNetworking.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.s2c; 2 | 3 | import dev.gegy.magic.client.casting.ClientCastingTracker; 4 | import dev.gegy.magic.client.casting.ConfiguredClientCasting; 5 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 6 | import net.minecraft.resources.ResourceLocation; 7 | import net.minecraft.world.entity.Entity; 8 | import net.minecraft.world.entity.player.Player; 9 | 10 | public final class MagicS2CNetworking { 11 | public static void registerReceivers() { 12 | ClientPlayNetworking.registerGlobalReceiver(SetCastingS2CPacket.CHANNEL, (client, handler, buf, responseSender) -> { 13 | final int sourceId = buf.readVarInt(); 14 | final ConfiguredClientCasting casting = ConfiguredClientCasting.CODEC.nullable().decode(buf); 15 | client.submit(() -> { 16 | try { 17 | final Entity source = client.level.getEntity(sourceId); 18 | if (source instanceof Player player) { 19 | ClientCastingTracker.INSTANCE.setCasting(player, casting); 20 | } 21 | } finally { 22 | buf.release(); 23 | } 24 | }); 25 | }); 26 | 27 | ClientPlayNetworking.registerGlobalReceiver(CastingEventS2CPacket.CHANNEL, (client, handler, buf, responseSender) -> { 28 | final int sourceId = buf.readVarInt(); 29 | final ResourceLocation id = buf.readResourceLocation(); 30 | buf.retain(); 31 | 32 | client.submit(() -> { 33 | try { 34 | final Entity source = client.level.getEntity(sourceId); 35 | if (source instanceof Player player) { 36 | ClientCastingTracker.INSTANCE.handleEvent(player, id, buf); 37 | } 38 | } finally { 39 | buf.release(); 40 | } 41 | }); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/network/s2c/SetCastingS2CPacket.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.network.s2c; 2 | 3 | import dev.gegy.magic.Magic; 4 | import dev.gegy.magic.client.casting.ConfiguredClientCasting; 5 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 6 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 7 | import net.minecraft.network.FriendlyByteBuf; 8 | import net.minecraft.resources.ResourceLocation; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.minecraft.world.entity.player.Player; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public final class SetCastingS2CPacket { 14 | static final ResourceLocation CHANNEL = Magic.identifier("set_casting"); 15 | 16 | public static FriendlyByteBuf create(final Player source, @Nullable final ConfiguredClientCasting casting) { 17 | final FriendlyByteBuf buf = PacketByteBufs.create(); 18 | buf.writeVarInt(source.getId()); 19 | ConfiguredClientCasting.CODEC.nullable().encode(casting, buf); 20 | return buf; 21 | } 22 | 23 | public static void sendTo(final ServerPlayer player, final FriendlyByteBuf buf) { 24 | ServerPlayNetworking.send(player, CHANNEL, buf); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/dev/gegy/magic/particle/MagicParticles.java: -------------------------------------------------------------------------------- 1 | package dev.gegy.magic.particle; 2 | 3 | import dev.gegy.magic.Magic; 4 | import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes; 5 | import net.minecraft.core.Registry; 6 | import net.minecraft.core.particles.SimpleParticleType; 7 | import net.minecraft.core.registries.BuiltInRegistries; 8 | 9 | public final class MagicParticles { 10 | public static final SimpleParticleType SPARK = FabricParticleTypes.simple(); 11 | 12 | public static void register() { 13 | Registry.register(BuiltInRegistries.PARTICLE_TYPE, Magic.identifier("spark"), SPARK); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/particles/spark.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": ["magic:spark"] 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/cloud_texture.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform vec3 Color; 4 | uniform float Time; 5 | 6 | in vec2 uv; 7 | 8 | out vec4 fragColor; 9 | 10 | #moj_import 11 | #moj_import 12 | 13 | float temperature_noise(vec3 p) { 14 | p *= vec3(0.5, 1.5, 1.5); 15 | float t = Time * 6.5; 16 | float a = noise(p * 0.4 + t * vec3(1.2, 0.2, -0.1)); 17 | float b = noise(p * 2.3 + t * vec3(1.8, -0.1, 0.3)); 18 | float c = noise(p * 1.6 + t * vec3(1.3, -0.3, 0.1)); 19 | return (a + b + c) / 3.0; 20 | } 21 | 22 | vec2 polar(vec2 p) { 23 | return vec2(length(p), atan(p.y, p.x)); 24 | } 25 | 26 | float sample_temperature(vec2 point) { 27 | point *= 2.0; 28 | 29 | vec2 polar = polar(point); 30 | float distance = polar.x; 31 | vec2 direction = point / distance; 32 | 33 | float noise = temperature_noise(vec3(polar.x, direction.xy)); 34 | noise = pow(noise, 3.0) * 1.6 - 0.1; 35 | 36 | return (1.0 - distance) + noise; 37 | } 38 | 39 | void main() { 40 | float temperature = sample_temperature(uv); 41 | fragColor = beam_color(temperature); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/end_texture.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform vec2 Scale; 4 | 5 | in vec2 Position; 6 | out vec2 uv; 7 | 8 | void main() { 9 | vec2 position = Position * 2.0 - 1.0; 10 | uv = position * 0.5 * Scale; 11 | gl_Position = vec4(position, 0.2, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/end_world.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec2 Position; 4 | 5 | uniform mat4 ModelViewProject; 6 | 7 | uniform float Scale; 8 | 9 | out vec2 uv; 10 | 11 | void main() { 12 | uv = Position * 0.5 + 0.5; 13 | gl_Position = ModelViewProject * vec4(Position * Scale, 0.0, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/impact_texture.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform vec3 Color; 4 | uniform float Time; 5 | 6 | in vec2 uv; 7 | 8 | out vec4 fragColor; 9 | 10 | #moj_import 11 | #moj_import 12 | 13 | float temperature_noise(vec3 p) { 14 | p *= vec3(0.5, 1.5, 1.5); 15 | float t = Time * 21.6; 16 | float a = noise(p * 1.7 + t * vec3(-1.2, 0.2, -0.1)); 17 | float b = noise(p * 4.2 + t * vec3(-1.8, -0.1, 0.3)); 18 | float c = noise(p * 3.1 + t * vec3(-1.3, -0.3, 0.1)); 19 | return (a + b + c) / 3.0; 20 | } 21 | 22 | vec2 polar(vec2 p) { 23 | return vec2(length(p), atan(p.y, p.x)); 24 | } 25 | 26 | float sample_temperature(vec2 point) { 27 | vec2 polar = polar(point); 28 | float distance = polar.x; 29 | vec2 direction = point / distance; 30 | 31 | float main = 1.0 - distance; 32 | float noise = pow(temperature_noise(vec3(polar.x, direction.xy)), 3.0) * 2.0 - 0.3; 33 | return main + noise; 34 | } 35 | 36 | void main() { 37 | float temperature = sample_temperature(uv); 38 | fragColor = beam_color(temperature); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/texture.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform vec3 Color; 4 | uniform float Time; 5 | uniform float Length; 6 | 7 | in vec2 uv; 8 | 9 | out vec4 fragColor; 10 | 11 | #moj_import 12 | #moj_import 13 | 14 | float temperature_noise(vec2 p) { 15 | p = p * vec2(0.5, 1.5); 16 | float t = Time * 21.6; 17 | float a = noise(p * 1.7 + t * vec2(-1.6, 0.5)); 18 | float b = noise(p * 4.2 + t * vec2(-1.5, -0.3)); 19 | float c = noise(p * 3.1 + t * vec2(-1.8, 0.2)); 20 | return (a + b + c) / 3.0; 21 | } 22 | 23 | float sample_temperature(vec2 point) { 24 | float pinch = 1.0 / min(point.x * point.x * 2.0, 1.0); 25 | point.y *= pinch; 26 | 27 | float jitter = (noise(Time * 50.0 - point.x) * 2.0 - 1.0) * 0.1; 28 | 29 | float main = 1.0 - abs(point.y + jitter); 30 | float noise = (temperature_noise(point) * 2.0 - 1.0) * 0.3 * pinch; 31 | 32 | return main + noise; 33 | } 34 | 35 | void main() { 36 | if (uv.x < Length) { 37 | float temperature = sample_temperature(uv); 38 | fragColor = beam_color(temperature); 39 | } else { 40 | fragColor = vec4(0.0); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/texture.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform vec2 Scale; 4 | 5 | in vec2 Position; 6 | out vec2 uv; 7 | 8 | void main() { 9 | uv = vec2(Position.x, Position.y - 0.5) * Scale; 10 | 11 | gl_Position = vec4(Position * 2.0 - 1.0, 0.2, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/beam/world.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec3 Position; 4 | in vec2 UV0; 5 | 6 | uniform mat4 ModelViewProject; 7 | 8 | uniform vec2 Scale; 9 | 10 | out vec2 uv; 11 | 12 | void main() { 13 | uv = UV0; 14 | 15 | vec3 local_position = vec3( 16 | Position.x * Scale.y, 17 | Position.y * Scale.y, 18 | Position.z * Scale.x 19 | ); 20 | gl_Position = ModelViewProject * vec4(local_position, 1.0); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/effect_world.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform sampler2D Sampler; 4 | 5 | in vec2 uv; 6 | 7 | out vec4 fragColor; 8 | 9 | void main() { 10 | vec4 color = texture(Sampler, uv); 11 | if (color.a < 0.1) { 12 | discard; 13 | } 14 | fragColor = vec4(color.rgb / color.a, color.a); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/glyph/texture.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform float TexelSize; 4 | 5 | in vec2 Position; 6 | out vec2 texel; 7 | 8 | void main() { 9 | vec2 uv = Position * 0.5; 10 | texel = uv / TexelSize; 11 | 12 | gl_Position = vec4(Position, 0.2, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/glyph/world.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec2 Position; 4 | 5 | uniform float Scale; 6 | 7 | uniform mat4 ModelViewProject; 8 | 9 | out vec2 uv; 10 | 11 | void main() { 12 | uv = (Position + 1.0) * 0.5; 13 | 14 | gl_Position = ModelViewProject * vec4(Position * Scale, 0.0, 1.0); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/include/beam_color.glsl: -------------------------------------------------------------------------------- 1 | float quantize(float x, float s) { 2 | return round(x / s) * s; 3 | } 4 | 5 | // from: 6 | vec3 hue_shift(vec3 color, float hue) { 7 | const vec3 k = vec3(0.57735, 0.57735, 0.57735); 8 | float cos_angle = cos(hue); 9 | return vec3(color * cos_angle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cos_angle)); 10 | } 11 | 12 | vec4 beam_color(float temperature) { 13 | temperature = quantize(temperature, 1.0 / 8.0); 14 | if (temperature <= 0.0) { 15 | discard; 16 | } 17 | 18 | float factor = clamp(temperature, 0.0, 1.0); 19 | 20 | vec3 color = mix(Color, vec3(1.2), factor); 21 | color = hue_shift(color, factor * 0.4); 22 | 23 | return vec4(color, 1.0); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/shaders/include/noise.glsl: -------------------------------------------------------------------------------- 1 | // 2 | // By Morgan McGuire @morgan3d, http://graphicscodex.com 3 | // 4 | float hash(float p) { 5 | p = fract(p * 0.011); 6 | p *= p + 7.5; 7 | p *= p + p; 8 | return fract(p); 9 | } 10 | 11 | float hash(vec2 p) { 12 | vec3 p3 = fract(vec3(p.xyx) * 0.13); 13 | p3 += dot(p3, p3.yzx + 3.333); 14 | return fract((p3.x + p3.y) * p3.z); 15 | } 16 | 17 | float noise(float x) { 18 | float i = floor(x); 19 | float f = fract(x); 20 | float u = f * f * (3.0 - 2.0 * f); 21 | return mix(hash(i), hash(i + 1.0), u); 22 | } 23 | 24 | float noise(vec2 x) { 25 | vec2 i = floor(x); 26 | vec2 f = fract(x); 27 | 28 | float a = hash(i); 29 | float b = hash(i + vec2(1.0, 0.0)); 30 | float c = hash(i + vec2(0.0, 1.0)); 31 | float d = hash(i + vec2(1.0, 1.0)); 32 | 33 | vec2 u = f * f * (3.0 - 2.0 * f); 34 | return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; 35 | } 36 | 37 | float noise(vec3 x) { 38 | const vec3 step = vec3(110, 241, 171); 39 | 40 | vec3 i = floor(x); 41 | vec3 f = fract(x); 42 | 43 | float n = dot(i, step); 44 | 45 | vec3 u = f * f * (3.0 - 2.0 * f); 46 | return mix( 47 | mix( 48 | mix(hash(n + dot(step, vec3(0, 0, 0))), hash(n + dot(step, vec3(1, 0, 0))), u.x), 49 | mix(hash(n + dot(step, vec3(0, 1, 0))), hash(n + dot(step, vec3(1, 1, 0))), u.x), 50 | u.y 51 | ), 52 | mix( 53 | mix(hash(n + dot(step, vec3(0, 0, 1))), hash(n + dot(step, vec3(1, 0, 1))), u.x), 54 | mix(hash(n + dot(step, vec3(0, 1, 1))), hash(n + dot(step, vec3(1, 1, 1))), u.x), 55 | u.y 56 | ), 57 | u.z 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/assets/magic/textures/particle/spark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gegy/magic/0529d0f7b5a2edc1d76e3c9b638d573b65e6428b/src/main/resources/assets/magic/textures/particle/spark.png -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "magic", 4 | "version": "${version}", 5 | "name": "Magic", 6 | "description": "", 7 | "authors": ["Gegy"], 8 | "license": "LGPLv3", 9 | "environment": "*", 10 | "entrypoints": { 11 | "main": ["dev.gegy.magic.Magic"], 12 | "client": ["dev.gegy.magic.MagicClient"] 13 | }, 14 | "mixins": ["magic.mixins.json"], 15 | "depends": { 16 | "fabricloader": ">=0.14", 17 | "fabric": ">=0.83", 18 | "minecraft": "1.20.x", 19 | "java": ">=17" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/magic.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "dev.gegy.magic.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | "ServerEntityMixin" 8 | ], 9 | "client": [ 10 | "client.ClientEntityCallbacksMixin", 11 | "client.PlayerMixin", 12 | "client.PlayerModelMixin" 13 | ], 14 | "injectors": { 15 | "defaultRequire": 1 16 | } 17 | } 18 | --------------------------------------------------------------------------------