first commit

This commit is contained in:
Patrick
2026-05-01 19:42:33 +02:00
commit e448a542dc
402 changed files with 23697 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
loom {
accessWidenerPath.set(file("src/main/resources/hero-api.accesswidener"))
}
dependencies {
api(project(":datatracker", configuration = "namedElements"))
modApi(libs.bundles.fabric)
modApi(libs.bundles.silk)
modApi(libs.bundles.performance)
modApi(libs.bundles.mongodb)
modApi(libs.bundles.hglaborutils) {
exclude(module = "fabric-api")
exclude(module = "hglabor-utils-events")
}
modApi(libs.owolib)
modApi(libs.geckolib)
modApi(libs.emoteLib)
}
@@ -0,0 +1,29 @@
package gg.norisk.heroes.common.mixin;
import gg.norisk.heroes.common.events.EntityEvents;
import net.minecraft.entity.Entity;
import net.minecraft.entity.data.TrackedData;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.HashMap;
import java.util.Map;
@Mixin(Entity.class)
public abstract class EntityMixin {
@Shadow
public abstract World getWorld();
@Unique
private final Map<String, Object> syncedValues = new HashMap<>();
@Inject(method = "onTrackedDataSet", at = @At("TAIL"))
private void injected(TrackedData<?> trackedData, CallbackInfo ci) {
EntityEvents.INSTANCE.getOnTrackedDataSetEvent().invoke(new EntityEvents.EntityTrackedDataSetEvent((Entity) (Object) this, trackedData));
}
}
@@ -0,0 +1,18 @@
package gg.norisk.heroes.common.mixin;
import com.llamalad7.mixinextras.sugar.Local;
import gg.norisk.heroes.common.HeroesManager;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.minecraft.resource.featuretoggle.FeatureManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(FeatureFlags.class)
public abstract class FeatureFlagsMixin {
@Inject(method = "<clinit>", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/featuretoggle/FeatureManager$Builder;build()Lnet/minecraft/resource/featuretoggle/FeatureManager;"))
private static void heroapi$featureFlag(CallbackInfo ci, @Local FeatureManager.Builder builder) {
HeroesManager.heroesFlag = builder.addFlag(HeroesManager.INSTANCE.toId("heroes"));
}
}
@@ -0,0 +1,41 @@
package gg.norisk.heroes.common.mixin;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import gg.norisk.heroes.common.events.EntityEvents;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin extends Entity {
@Shadow
public abstract boolean damage(DamageSource source, float amount);
public LivingEntityMixin(EntityType<?> entityType, World world) {
super(entityType, world);
}
@Inject(method = "tickMovement", at = @At("HEAD"))
private void tickMovementEvent(CallbackInfo ci) {
EntityEvents.INSTANCE.getLivingEntityTickMovementEvent().invoke(new EntityEvents.LivingEntityEvent((LivingEntity) (Object) this));
}
@ModifyReturnValue(method = "computeFallDamage", at = @At("RETURN"))
private int injected(int original, float fallDistance, float damageMultiplier) {
var event = new EntityEvents.ComputeFallDamageEvent(fallDistance, damageMultiplier, original, (LivingEntity) (Object) this);
EntityEvents.INSTANCE.getComputeFallDamageEvent().invoke(event);
if (event.getFallDamage() != null) {
return event.getFallDamage();
} else {
return original;
}
}
}
@@ -0,0 +1,16 @@
package gg.norisk.heroes.common.mixin;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Map;
@Mixin(MinecraftServer.class)
public interface MinecraftServerAccessor {
@Accessor("worlds")
public Map<RegistryKey<World>, ServerWorld> getLevelsMap();
}
@@ -0,0 +1,31 @@
package gg.norisk.heroes.common.mixin;
import com.llamalad7.mixinextras.sugar.Local;
import gg.norisk.heroes.common.HeroesManager;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.resource.DataConfiguration;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.server.MinecraftServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Set;
@Mixin(MinecraftServer.class)
public abstract class MinecraftServerMixin {
@Inject(method = "loadDataPacks(Lnet/minecraft/resource/ResourcePackManager;Lnet/minecraft/resource/DataConfiguration;ZZ)Lnet/minecraft/resource/DataConfiguration;", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false))
private static void heroapi$forceDataPack(ResourcePackManager resourcePackManager, DataConfiguration dataConfiguration, boolean bl, boolean bl2, CallbackInfoReturnable<DataConfiguration> cir,
@Local Set<String> set, @Local ResourcePackProfile profile
) {
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER) {
HeroesManager.INSTANCE.getLogger().info("Found Datapack {}", profile.getId());
if ("heroes".equalsIgnoreCase(profile.getId())) {
set.add(profile.getId());
}
}
}
}
@@ -0,0 +1,29 @@
package gg.norisk.heroes.common.mixin;
import gg.norisk.heroes.common.hero.IHeroManagerKt;
import gg.norisk.heroes.common.hero.ability.AbstractAbility;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PlayerEntity.class)
public abstract class PlayerEntityMixin extends LivingEntity {
protected PlayerEntityMixin(EntityType<? extends LivingEntity> entityType, World world) {
super(entityType, world);
}
@Inject(method = "tick", at = @At("HEAD"))
private void tickInjection(CallbackInfo ci) {
var player = (PlayerEntity) (Object) this;
var hero = IHeroManagerKt.getHero(player);
if (hero == null) return;
for (AbstractAbility<?> ability : hero.getAbilities().values()) {
ability.onTick(player);
}
}
}
@@ -0,0 +1,26 @@
package gg.norisk.heroes.common.mixin;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ResourcePackProvider;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Set;
@Mixin(value = ResourcePackManager.class, priority = 2000)
public abstract class ResourcePackManagerMixin {
@Mutable
@Shadow
@Final
private Set<ResourcePackProvider> providers;
@Inject(method = "<init>", at = @At("RETURN"))
private void heroapi$resourcePack(ResourcePackProvider[] resources, CallbackInfo ci) {
//this.providers.add(new HeroDataPackProvider(new SymlinkFinder(path -> true)));
}
}
@@ -0,0 +1,14 @@
package gg.norisk.heroes.common.mixin;
import net.minecraft.entity.Entity;
import net.minecraft.world.World;
import net.minecraft.world.entity.EntityLookup;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(World.class)
public interface WorldAccessor {
@Invoker("getEntityLookup")
EntityLookup<Entity> invokeGetEntityLookup();
}
@@ -0,0 +1,47 @@
package gg.norisk.heroes.common.mixin.client;
import gg.norisk.heroes.client.events.ClientEvents;
import gg.norisk.heroes.client.renderer.CameraShaker;
import gg.norisk.heroes.client.ui.OrthoCamera;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@ModifyConstant(method = "update", constant = @Constant(floatValue = 4.0f))
private float updateInjection(float constant) {
var event = new ClientEvents.CameraClipToSpaceEvent(constant);
ClientEvents.INSTANCE.getCameraClipToSpaceEvent().invoke(event);
return (float) event.getValue();
}
@Inject(
method = "update",
at = @At(
// Inject before the call to clipToSpace
value = "INVOKE",
target = "Lnet/minecraft/client/render/Camera;setPos(DDD)V",
shift = At.Shift.BY,
by = 1
)
)
void camerashake$onUpdate(BlockView area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickDelta, CallbackInfo ci) {
double x = CameraShaker.INSTANCE.getAvgX();
double y = CameraShaker.INSTANCE.getAvgY();
((Camera) (Object)this).moveBy((float) .0, (float) y, (float) x);
}
@ModifyVariable(method = "moveBy", at = @At("HEAD"), index = 1, argsOnly = true)
private float heroapi$moveByHeadX(float value) {
return OrthoCamera.INSTANCE.isEnabled() ? 0.0f : value;
}
@ModifyVariable(method = "moveBy", at = @At("HEAD"), index = 3, argsOnly = true)
private float heroapi$moveByHeadZ(float value) {
return OrthoCamera.INSTANCE.isEnabled() ? 0.0f : value;
}
}
@@ -0,0 +1,38 @@
package gg.norisk.heroes.common.mixin.client;
import com.mojang.authlib.GameProfile;
import gg.norisk.heroes.common.events.AfterTickInputEvent;
import gg.norisk.heroes.common.events.BasicEventsKt;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.input.Input;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.entity.player.PlayerEntity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayerEntity.class)
public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity {
@Shadow
public Input input;
@Shadow
@Final
protected MinecraftClient client;
public ClientPlayerEntityMixin(ClientWorld clientWorld, GameProfile gameProfile) {
super(clientWorld, gameProfile);
}
@Inject(method = "tickMovement", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/input/Input;tick(ZF)V", shift = At.Shift.AFTER))
public void inputHandle(CallbackInfo ci) {
PlayerEntity player = MinecraftClient.getInstance().player;
if (!MinecraftClient.getInstance().isRunning() || player == null) return;
BasicEventsKt.getAfterTickInputEvent().invoke(new AfterTickInputEvent(this.input));
}
}
@@ -0,0 +1,21 @@
package gg.norisk.heroes.common.mixin.client;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import gg.norisk.heroes.common.events.EntityEvents;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderDispatcher;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(EntityRenderDispatcher.class)
public abstract class EntityRenderDispatcherMixin {
@WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/EntityRenderer;render(Lnet/minecraft/entity/Entity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V"))
private boolean onlyRenderIfAllowed(EntityRenderer<Entity> targetClass, Entity entity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i) {
var event = new EntityEvents.EntityRendererEvent(entity, f, g, matrixStack, vertexConsumerProvider, i);
EntityEvents.INSTANCE.getEntityRendererEvent().invoke(event);
return !event.isCancelled().get();
}
}
@@ -0,0 +1,21 @@
package gg.norisk.heroes.common.mixin.client;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import gg.norisk.heroes.client.ui.OrthoCamera;
import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.GameOptions;
import net.minecraft.client.option.Perspective;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(GameOptions.class)
public abstract class GameOptionsMixin {
@ModifyReturnValue(method = "getPerspective", at = @At("RETURN"))
private Perspective heroapi$GetPerspective(Perspective original) {
if (OrthoCamera.INSTANCE.isEnabled()) {
return Perspective.THIRD_PERSON_FRONT;
}
return original;
}
}
@@ -0,0 +1,126 @@
package gg.norisk.heroes.common.mixin.client;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.systems.VertexSorter;
import gg.norisk.heroes.client.renderer.CameraShaker;
import gg.norisk.heroes.client.ui.OrthoCamera;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.GameRenderer;
import net.minecraft.client.render.RenderTickCounter;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(GameRenderer.class)
public abstract class GameRendererMixin {
@Shadow
@Final
private MinecraftClient client;
@Inject(
method = "render",
at = @At("HEAD")
)
private void heroapi$onRender(RenderTickCounter renderTickCounter, boolean tick, CallbackInfo ci) {
if (!client.skipGameRender && tick && client.world != null) {
CameraShaker.INSTANCE.newFrame();
}
}
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;render(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
)
private boolean heroapi$dontRenderHud(InGameHud instance, DrawContext drawContext, RenderTickCounter renderTickCounter) {
return !OrthoCamera.INSTANCE.isEnabled();
}
@WrapWithCondition(
method = "renderWorld",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;renderHand(Lnet/minecraft/client/render/Camera;FLorg/joml/Matrix4f;)V")
)
private boolean heroapi$dontRenderHand(GameRenderer instance, Camera camera, float f, Matrix4f matrix4f) {
return !OrthoCamera.INSTANCE.isEnabled();
}
@Inject(
method = "renderHand",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/render/GameRenderer;tiltViewWhenHurt(Lnet/minecraft/client/util/math/MatrixStack;F)V"
)
)
private void heroapi$shakeHand(Camera camera, float f, Matrix4f matrix4f, CallbackInfo ci) {
float x = (float) CameraShaker.INSTANCE.getAvgX();
float y = (float) CameraShaker.INSTANCE.getAvgY();
matrix4f.translate(x, -y, (float) .0); // opposite of camera
}
// TODO keine ahnung es will nxi so wie ich will T_T
@ModifyArg(
method = "renderWorld",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/render/WorldRenderer;setupFrustum(Lnet/minecraft/util/math/Vec3d;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;)V"
),
index = 2
)
private Matrix4f heroapi$orthoFrustumProjMat(Matrix4f projMat) {
if (OrthoCamera.INSTANCE.isEnabled()) {
return OrthoCamera.INSTANCE.createOrthoMatrix(1.0F, 20.0F);
}
return projMat;
}
@ModifyArg(
method = "renderWorld",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/render/WorldRenderer;render(Lnet/minecraft/client/render/RenderTickCounter;ZLnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/GameRenderer;Lnet/minecraft/client/render/LightmapTextureManager;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;)V"
),
index = 6
)
private Matrix4f heroapi$orthoProjMat(Matrix4f projMat, @Local(argsOnly = true) RenderTickCounter tickCounter) {
if (OrthoCamera.INSTANCE.isEnabled()) {
float tickDelta = tickCounter.getTickDelta(true);
Matrix4f mat = OrthoCamera.INSTANCE.createOrthoMatrix(tickDelta, 0.0F);
RenderSystem.setProjectionMatrix(mat, VertexSorter.BY_Z);
return mat;
}
return projMat;
}
@ModifyExpressionValue(
method = "renderWorld",
at = @At(
value = "INVOKE",
target = "Lorg/joml/Quaternionf;conjugate(Lorg/joml/Quaternionf;)Lorg/joml/Quaternionf;",
remap = false
)
)
private Quaternionf heroapi$modifyRotation(Quaternionf original, @Local(argsOnly = true) RenderTickCounter tickCounter) {
if (!OrthoCamera.INSTANCE.isEnabled()) {
return original;
}
return original.rotationXYZ(
OrthoCamera.INSTANCE.handlePitch(original, tickCounter.getTickDelta(false)),
OrthoCamera.INSTANCE.handleYaw(original, tickCounter.getTickDelta(false)),
0.0F
);
}
}
@@ -0,0 +1,35 @@
package gg.norisk.heroes.common.mixin.client;
import gg.norisk.heroes.client.events.ClientEvents;
import gg.norisk.heroes.common.events.BasicEventsKt;
import gg.norisk.heroes.common.events.MouseScrollEvent;
import net.minecraft.client.Mouse;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Mouse.class)
public abstract class MouseMixin {
@Inject(method = "onMouseScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getOverlay()Lnet/minecraft/client/gui/screen/Overlay;", shift = At.Shift.BEFORE))
private void hookMouseScroll(long window, double horizontal, double vertical, CallbackInfo callbackInfo) {
BasicEventsKt.getMouseScrollEvent().invoke(new MouseScrollEvent(window, horizontal, vertical));
}
@Inject(
method = "onMouseScroll(JDD)V",
at = @At(
value = "FIELD",
target = "Lnet/minecraft/client/Mouse;eventDeltaVerticalWheel:D",
ordinal = 6
),
cancellable = true
)
private void updateZoom(CallbackInfo info) {
var event = new ClientEvents.PreHotbarScrollEvent();
ClientEvents.INSTANCE.getPreHotbarScrollEvent().invoke(event);
if (event.isCancelled().get()) {
info.cancel();
}
}
}
@@ -0,0 +1,83 @@
package gg.norisk.heroes.common.mixin.client;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import gg.norisk.heroes.client.renderer.SkinUtils;
import gg.norisk.heroes.common.hero.IHeroManagerKt;
import gg.norisk.heroes.common.player.FFAPlayerKt;
import net.minecraft.client.model.ModelPart;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRendererFactory;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.PlayerEntityRenderer;
import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.util.SkinTextures;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.EntityAttachmentType;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerEntityRenderer.class)
public abstract class PlayerEntityRendererMixin extends LivingEntityRenderer<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> {
public PlayerEntityRendererMixin(EntityRendererFactory.Context ctx, PlayerEntityModel<AbstractClientPlayerEntity> model, float shadowRadius) {
super(ctx, model, shadowRadius);
}
@Inject(method = "getTexture(Lnet/minecraft/client/network/AbstractClientPlayerEntity;)Lnet/minecraft/util/Identifier;", at = @At("RETURN"), cancellable = true)
private void getSkinTextureInjection(AbstractClientPlayerEntity player, CallbackInfoReturnable<Identifier> cir) {
var hero = IHeroManagerKt.getHero(player);
if (hero == null) return;
var skin = hero.getInternalCallbacks().getGetSkin();
if (skin != null) {
cir.setReturnValue(skin.invoke(player));
}
}
@ModifyReturnValue(
method = "getTexture(Lnet/minecraft/client/network/AbstractClientPlayerEntity;)Lnet/minecraft/util/Identifier;",
at = @At("RETURN")
)
private Identifier redirectHeroSkin(Identifier original, AbstractClientPlayerEntity abstractClientPlayerEntity) {
return SkinUtils.INSTANCE.redirectCombinedSkin(original, abstractClientPlayerEntity);
}
@Inject(method = "renderLabelIfPresent(Lnet/minecraft/client/network/AbstractClientPlayerEntity;Lnet/minecraft/text/Text;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IF)V", at = @At(value = "HEAD"))
private void heroapi$renderBounty(AbstractClientPlayerEntity abstractClientPlayerEntity, Text text, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, float f, CallbackInfo ci) {
renderBounty(this.dispatcher.getSquaredDistanceToCamera(abstractClientPlayerEntity), abstractClientPlayerEntity, matrixStack, vertexConsumerProvider, i, f);
}
@Unique
private void renderBounty(double d, AbstractClientPlayerEntity abstractClientPlayerEntity, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, float f) {
if (d < (double) 2000.0F) {
int bounty = FFAPlayerKt.getFfaBounty(abstractClientPlayerEntity);
if (bounty > 0) {
Vec3d vec3d = abstractClientPlayerEntity.getAttachments().getPointNullable(EntityAttachmentType.NAME_TAG, 0, abstractClientPlayerEntity.getYaw(f));
if (vec3d != null) {
matrixStack.push();
matrixStack.scale(0.5f, 0.5f, 0.5f);
matrixStack.translate(vec3d.x, vec3d.y + 0.125f, vec3d.z);
super.renderLabelIfPresent(abstractClientPlayerEntity, Text.empty().append("Bounty: ").append(String.valueOf(bounty)), matrixStack, vertexConsumerProvider, i, f);
matrixStack.pop();
matrixStack.translate(0.0F, 0.075F, 0.0F);
}
}
}
}
@WrapOperation(
method = "renderArm",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;texture()Lnet/minecraft/util/Identifier;")
)
private Identifier heroapi$redirectRenderArmSkinTexture(SkinTextures instance, Operation<Identifier> original, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, AbstractClientPlayerEntity abstractClientPlayerEntity, ModelPart modelPart, ModelPart modelPart2) {
return SkinUtils.INSTANCE.redirectCombinedSkin(original.call(instance), abstractClientPlayerEntity);
}
}
@@ -0,0 +1,33 @@
package gg.norisk.heroes.common.mixin.client;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import gg.norisk.heroes.client.renderer.SkinUtils;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.hud.PlayerListHud;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.client.util.SkinTextures;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(PlayerListHud.class)
public abstract class PlayerListHudMixin {
@Shadow
@Final
private MinecraftClient client;
@WrapOperation(
method = "render",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;texture()Lnet/minecraft/util/Identifier;")
)
private Identifier heroapi$redirectCombinedSkin(SkinTextures instance, Operation<Identifier> original, @Local PlayerListEntry playerListEntry) {
var player = client.world == null ? null : client.world.getPlayerByUuid(playerListEntry.getProfile().getId());
return SkinUtils.INSTANCE.redirectCombinedSkin(original.call(instance), ((AbstractClientPlayerEntity) player));
}
}
@@ -0,0 +1,28 @@
package gg.norisk.heroes.common.mixin.client.compat;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import gg.norisk.heroes.client.ui.OrthoCamera;
import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen;
import me.jellysquid.mods.sodium.client.gl.device.RenderDevice;
import me.jellysquid.mods.sodium.client.render.chunk.DefaultChunkRenderer;
import me.jellysquid.mods.sodium.client.render.chunk.ShaderChunkRenderer;
import me.jellysquid.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType;
import net.minecraft.client.MinecraftClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(value = DefaultChunkRenderer.class, remap = false)
public abstract class DefaultChunkRendererMixin extends ShaderChunkRenderer {
public DefaultChunkRendererMixin(RenderDevice device, ChunkVertexType vertexType) {
super(device, vertexType);
}
@ModifyExpressionValue(
remap = false,
method = "render",
at = @At(value = "FIELD", target = "Lme/jellysquid/mods/sodium/client/gui/SodiumGameOptions$PerformanceSettings;useBlockFaceCulling:Z", remap = false)
)
private boolean ffa$blockFaceCulling(boolean original) {
return original && !(OrthoCamera.INSTANCE.isEnabled());
}
}
@@ -0,0 +1,310 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package gg.norisk.heroes.common.ui;
import gg.norisk.heroes.client.ui.skilltree.AbilitySkillTreeComponent;
import io.wispforest.owo.ui.container.FlowLayout;
import io.wispforest.owo.ui.container.WrappingParentComponent;
import io.wispforest.owo.ui.core.*;
import io.wispforest.owo.ui.util.Delta;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Function;
public class ScrollContainerV2<C extends Component> extends WrappingParentComponent<C> {
public static final Identifier VERTICAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_vertical");
public static final Identifier DISABLED_VERTICAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_vertical_disabled");
public static final Identifier HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_horizontal_disabled");
public static final Identifier DISABLED_HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_horizontal_disabled");
public static final Identifier VANILLA_SCROLLBAR_TRACK_TEXTURE = Identifier.of("owo", "scrollbar/track");
public static final Identifier FLAT_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_flat");
protected double scrollOffsetVertical = (double) 0.0F;
protected double scrollOffsetHorizontal = (double) 0.0F;
protected double currentScrollPositionVertical = (double) 0.0F;
protected double currentScrollPositionHorizontal = (double) 0.0F;
protected int lastScrollPositionVertical = -1;
protected int lastScrollPositionHorizontal = -1;
protected int scrollStep = 0;
protected int fixedScrollbarLength = 0;
protected double lastScrollbarLengthVertical = (double) 0.0F;
protected double lastScrollbarLengthHorizontal = (double) 0.0F;
protected boolean scrollbaring = false;
protected int maxScrollVertical = 0;
protected int maxScrollHorziontal = 0;
protected int childSize = 0;
protected final ScrollDirection verticalDirection = ScrollDirection.VERTICAL;
protected final ScrollDirection horizontalDirection = ScrollDirection.HORIZONTAL;
private boolean init = true;
public ScrollContainerV2(Sizing horizontalSizing, Sizing verticalSizing, C child) {
super(horizontalSizing, verticalSizing, child);
this.scrollOffsetHorizontal = maxScrollHorziontal - maxScrollHorziontal / 2.0;
this.currentScrollPositionHorizontal = scrollOffsetHorizontal;
}
protected int determineHorizontalContentSize(Sizing sizing) {
if (this.verticalDirection == ScrollDirection.VERTICAL) {
return super.determineHorizontalContentSize(sizing);
} else {
throw new UnsupportedOperationException("Horizontal ScrollContainer cannot be horizontally content-sized");
}
}
protected int determineVerticalContentSize(Sizing sizing) {
if (this.horizontalDirection == ScrollDirection.HORIZONTAL) {
return super.determineVerticalContentSize(sizing);
} else {
throw new UnsupportedOperationException("Vertical ScrollContainer cannot be vertically content-sized");
}
}
public void layout(Size space) {
super.layout(space);
this.maxScrollVertical = Math.max(0, (Integer) this.verticalDirection.sizeGetter.apply(this.child) - ((Integer) this.verticalDirection.sizeGetter.apply(this) - (Integer) this.verticalDirection.insetGetter.apply((Insets) this.padding.get())));
this.maxScrollHorziontal = Math.max(0, (Integer) this.horizontalDirection.sizeGetter.apply(this.child) - ((Integer) this.horizontalDirection.sizeGetter.apply(this) - (Integer) this.horizontalDirection.insetGetter.apply((Insets) this.padding.get())));
this.scrollOffsetVertical = MathHelper.clamp(this.scrollOffsetVertical, (double) 0.0F, (double) this.maxScrollVertical + (double) 0.5F);
this.scrollOffsetHorizontal = MathHelper.clamp(this.scrollOffsetHorizontal, (double) 0.0F, (double) this.maxScrollHorziontal + (double) 0.5F);
this.childSize = (Integer) this.verticalDirection.sizeGetter.apply(this.child);
this.lastScrollPositionVertical = -1;
this.lastScrollPositionHorizontal = -1;
}
protected int childMountX() {
return (int) ((double) super.childMountX() - this.verticalDirection.choose(this.currentScrollPositionVertical, (double) 0.0F));
}
protected int childMountY() {
return (int) ((double) super.childMountY() - this.verticalDirection.choose((double) 0.0F, this.currentScrollPositionVertical));
}
protected void parentUpdate(float delta, int mouseX, int mouseY) {
super.parentUpdate(delta, mouseX, mouseY);
this.currentScrollPositionVertical += Delta.compute(this.currentScrollPositionVertical, this.scrollOffsetVertical, (double) delta * (double) 0.5F);
this.currentScrollPositionHorizontal += Delta.compute(this.currentScrollPositionHorizontal, this.scrollOffsetHorizontal, (double) delta * (double) 0.5F);
}
public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) {
if (init) {
//for centering
init = false;
this.scrollOffsetHorizontal = maxScrollHorziontal - maxScrollHorziontal / 2.0;
this.currentScrollPositionHorizontal = scrollOffsetHorizontal;
}
super.draw(context, mouseX, mouseY, partialTicks, delta);
int effectiveScrollOffsetVertical = this.scrollStep > 0 ? (int) this.scrollOffsetVertical / this.scrollStep * this.scrollStep : (int) this.currentScrollPositionVertical;
if (this.scrollStep > 0 && (double) this.maxScrollVertical - this.scrollOffsetVertical == (double) -1.0F) {
effectiveScrollOffsetVertical = (int) ((double) effectiveScrollOffsetVertical + this.scrollOffsetVertical % (double) this.scrollStep);
}
int newScrollPositionVertical = this.verticalDirection.coordinateGetter.apply(this) - effectiveScrollOffsetVertical;
if (newScrollPositionVertical != this.lastScrollPositionVertical) {
this.verticalDirection.coordinateSetter.accept(this.child, newScrollPositionVertical + (this.padding.get().top() + this.child.margins().get().top()));
this.lastScrollPositionVertical = newScrollPositionVertical;
}
//HORIZONTAL
int effectiveScrollOffsetHorizontal = this.scrollStep > 0 ? (int) this.scrollOffsetHorizontal / this.scrollStep * this.scrollStep : (int) this.currentScrollPositionHorizontal;
if (this.scrollStep > 0 && (double) this.maxScrollHorziontal - this.scrollOffsetHorizontal == (double) -1.0F) {
effectiveScrollOffsetHorizontal = (int) ((double) effectiveScrollOffsetHorizontal + this.scrollOffsetHorizontal % (double) this.scrollStep);
}
int newScrollPositionHorizontal = this.horizontalDirection.coordinateGetter.apply(this) - effectiveScrollOffsetHorizontal;
if (newScrollPositionHorizontal != this.lastScrollPositionHorizontal) {
this.horizontalDirection.coordinateSetter.accept(this.child, newScrollPositionHorizontal + (this.padding.get().left() + this.child.margins().get().left()));
this.lastScrollPositionHorizontal = newScrollPositionHorizontal;
}
context.getMatrices().push();
double visualOffsetVertical = -(this.currentScrollPositionVertical % (double) 1.0F);
if (visualOffsetVertical > 0.9999999 || visualOffsetVertical < 1.0E-7) {
visualOffsetVertical = (double) 0.0F;
}
double visualOffsetHorizontal = -(this.currentScrollPositionHorizontal % (double) 1.0F);
if (visualOffsetHorizontal > 0.9999999 || visualOffsetHorizontal < 1.0E-7) {
visualOffsetHorizontal = (double) 0.0F;
}
context.getMatrices().translate(this.horizontalDirection.choose(visualOffsetHorizontal, 0.0F), this.verticalDirection.choose(0.0F, visualOffsetVertical), 0.0F);
this.drawChildren(context, mouseX, mouseY, partialTicks, delta, this.childView);
context.getMatrices().pop();
Insets padding = this.padding.get();
int selfSizeVertical = this.verticalDirection.sizeGetter.apply(this);
int contentSizeVertical = this.verticalDirection.sizeGetter.apply(this) - this.verticalDirection.insetGetter.apply(padding);
this.lastScrollbarLengthVertical = this.fixedScrollbarLength == 0 ? Math.min(Math.floor((float) selfSizeVertical / (float) this.childSize * (float) contentSizeVertical), contentSizeVertical) : (double) this.fixedScrollbarLength;
int selfSizeHorizontal = this.horizontalDirection.sizeGetter.apply(this);
int contentSizeHorizontal = this.horizontalDirection.sizeGetter.apply(this) - this.horizontalDirection.insetGetter.apply(padding);
this.lastScrollbarLengthHorizontal = this.fixedScrollbarLength == 0 ? Math.min(Math.floor((float) selfSizeHorizontal / (float) this.childSize * (float) contentSizeHorizontal), contentSizeHorizontal) : (double) this.fixedScrollbarLength;
}
public boolean canFocus(FocusSource source) {
return true;
}
public boolean onMouseScroll(double mouseX, double mouseY, double amount) {
if (this.child.onMouseScroll((double) this.x + mouseX - (double) this.child.x(), (double) this.y + mouseY - (double) this.child.y(), amount)) {
return true;
} else {
if (this.scrollStep < 1) {
this.scrollByVertical(-amount * (double) 15.0F, false, true);
} else {
this.scrollByVertical(-amount * (double) this.scrollStep, true, true);
}
return true;
}
}
public boolean onMouseDown(double mouseX, double mouseY, int button) {
if (this.isInScrollbar((double) this.x + mouseX, (double) this.y + mouseY)) {
super.onMouseDown(mouseX, mouseY, button);
return true;
} else {
return super.onMouseDown(mouseX, mouseY, button);
}
}
public boolean onMouseDrag(double mouseX, double mouseY, double deltaX, double deltaY, int button) {
if (!this.scrollbaring && !this.isInScrollbar((double) this.x + mouseX, (double) this.y + mouseY)) {
return super.onMouseDrag(mouseX, mouseY, deltaX, deltaY, button);
} else {
double deltaVertical = this.verticalDirection.choose(deltaX, deltaY) * -1;
double selfSizeVertical = this.verticalDirection.sizeGetter.apply(this) - this.verticalDirection.insetGetter.apply(this.padding.get());
double scalarVertical = (double) this.maxScrollVertical / (selfSizeVertical - this.lastScrollbarLengthVertical);
if (!Double.isFinite(scalarVertical)) {
scalarVertical = 0.0F;
}
this.scrollByVertical(deltaVertical * scalarVertical, true, false);
double deltaHorizontal = this.horizontalDirection.choose(deltaX, deltaY) * -1;
double selfSizeHorizontal = this.horizontalDirection.sizeGetter.apply(this) - this.horizontalDirection.insetGetter.apply(this.padding.get());
double scalarHorizontal = (double) this.maxScrollHorziontal / (selfSizeHorizontal - this.lastScrollbarLengthVertical);
if (!Double.isFinite(scalarHorizontal)) {
scalarHorizontal = 0.0F;
}
this.scrollByHorizontal(deltaHorizontal * scalarHorizontal, true, false);
this.scrollbaring = true;
return true;
}
}
public boolean onKeyPress(int keyCode, int scanCode, int modifiers) {
if (keyCode == this.verticalDirection.lessKeycode) {
this.scrollByVertical((double) -10.0F, false, true);
} else if (keyCode == this.verticalDirection.moreKeycode) {
this.scrollByVertical((double) 10.0F, false, true);
} else if (keyCode == 267) {
this.scrollByVertical(this.verticalDirection.choose((double) this.width, (double) this.height) * 0.8, false, true);
} else if (keyCode == 266) {
this.scrollByVertical(this.verticalDirection.choose((double) this.width, (double) this.height) * -0.8, false, true);
}
return false;
}
public boolean onMouseUp(double mouseX, double mouseY, int button) {
this.scrollbaring = false;
return true;
}
public @Nullable Component childAt(int x, int y) {
return this.isInScrollbar((double) x, (double) y) ? this : super.childAt(x, y);
}
protected void scrollByVertical(double offset, boolean instant, boolean showScrollbar) {
this.scrollOffsetVertical = MathHelper.clamp(this.scrollOffsetVertical + offset, (double) 0.0F, (double) this.maxScrollVertical + (double) 0.5F);
if (instant) {
this.currentScrollPositionVertical = this.scrollOffsetVertical;
}
}
protected void scrollByHorizontal(double offset, boolean instant, boolean showScrollbar) {
this.scrollOffsetHorizontal = MathHelper.clamp(this.scrollOffsetHorizontal + offset, (double) 0.0F, (double) this.maxScrollHorziontal + (double) 0.5F);
if (instant) {
this.currentScrollPositionHorizontal = this.scrollOffsetHorizontal;
}
}
protected boolean isInScrollbar(double mouseX, double mouseY) {
return true;
}
public ScrollContainerV2<C> scrollTo(Component component) {
this.scrollOffsetVertical = MathHelper.clamp(this.scrollOffsetVertical - (double) (this.y - component.y() + ((Insets) component.margins().get()).top()), (double) 0.0F, (double) this.maxScrollVertical);
this.scrollOffsetHorizontal = MathHelper.clamp(this.scrollOffsetHorizontal - (double) (this.x - component.x() + ((Insets) component.margins().get()).right()) - component.width() * 4 - 28, (double) 0.0F, (double) this.maxScrollHorziontal);
return this;
}
public ScrollContainerV2<C> scrollTo(@Range(
from = 0L,
to = 1L
) double horizontal, double vertical) {
this.scrollOffsetVertical = (double) this.maxScrollVertical * vertical;
this.scrollOffsetHorizontal = (double) this.maxScrollHorziontal * horizontal;
return this;
}
public ScrollContainerV2<C> scrollStep(int scrollStep) {
this.scrollStep = scrollStep;
return this;
}
public int scrollStep() {
return this.scrollStep;
}
public ScrollContainerV2<C> fixedScrollbarLength(int fixedScrollbarLength) {
this.fixedScrollbarLength = fixedScrollbarLength;
return this;
}
public int fixedScrollbarLength() {
return this.fixedScrollbarLength;
}
public static enum ScrollDirection {
VERTICAL(Component::height, Component::updateY, Component::y, Insets::vertical, 265, 264),
HORIZONTAL(Component::width, Component::updateX, Component::x, Insets::horizontal, 263, 262);
public final Function<Component, Integer> sizeGetter;
public final BiConsumer<Component, Integer> coordinateSetter;
public final Function<ScrollContainerV2<?>, Integer> coordinateGetter;
public final Function<Insets, Integer> insetGetter;
public final int lessKeycode;
public final int moreKeycode;
private ScrollDirection(Function<Component, Integer> sizeGetter, BiConsumer<Component, Integer> coordinateSetter, Function<ScrollContainerV2<?>, Integer> coordinateGetter, Function<Insets, Integer> insetGetter, int lessKeycode, int moreKeycode) {
this.sizeGetter = sizeGetter;
this.coordinateSetter = coordinateSetter;
this.coordinateGetter = coordinateGetter;
this.insetGetter = insetGetter;
this.lessKeycode = lessKeycode;
this.moreKeycode = moreKeycode;
}
public double choose(double horizontal, double vertical) {
double var10000;
switch (this.ordinal()) {
case 0 -> var10000 = vertical;
case 1 -> var10000 = horizontal;
default -> throw new MatchException((String) null, (Throwable) null);
}
return var10000;
}
}
}
@@ -0,0 +1,32 @@
package gg.norisk.heroes.client
import gg.norisk.heroes.client.command.ClientHeroCommand
import gg.norisk.heroes.client.config.ConfigManagerClient
import gg.norisk.heroes.client.hero.ability.AbilityKeyBindManager
import gg.norisk.heroes.client.hero.ability.AbilityManagerClient
import gg.norisk.heroes.client.networking.MouseListener
import gg.norisk.heroes.client.option.HeroKeyBindings
import gg.norisk.heroes.client.renderer.CameraShaker
import gg.norisk.heroes.client.renderer.KeyBindHud
import gg.norisk.heroes.client.renderer.Speedlines
import gg.norisk.heroes.client.ui.OrthoCamera
import gg.norisk.heroes.common.HeroesManager.logger
import net.fabricmc.api.ClientModInitializer
object HeroesManagerClient : ClientModInitializer {
override fun onInitializeClient() {
logger.info("Init Hero client...")
HeroKeyBindings.initClient()
ConfigManagerClient.init()
AbilityManagerClient.init()
OrthoCamera.initClient()
AbilityKeyBindManager.initializeKeyBindListeners()
KeyBindHud.init()
MouseListener.initClient()
Speedlines.initClient()
CameraShaker.initClient()
ClientHeroCommand.init()
}
}
@@ -0,0 +1,29 @@
package gg.norisk.heroes.client.command
import gg.norisk.heroes.client.config.ConfigManagerClient
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.player.ffaPlayer
import kotlinx.serialization.encodeToString
import net.silkmc.silk.commands.clientCommand
import net.silkmc.silk.commands.player
import net.silkmc.silk.core.text.literalText
object ClientHeroCommand {
fun init() {
clientCommand("heroes-client") {
requires { it.enabledFeatures.contains(HeroesManager.heroesFlag) }
literal("debug") {
literal("printffaplayer") {
runs {
val player = this.source.player
player.sendMessage(literalText {
text("FFA Player:")
emptyLine()
text(ConfigManagerClient.JSON.encodeToString(player.ffaPlayer))
})
}
}
}
}
}
}
@@ -0,0 +1,27 @@
package gg.norisk.heroes.client.config
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.HeroManager
import gg.norisk.heroes.common.networking.Networking
import kotlinx.serialization.json.Json
import net.silkmc.silk.core.task.mcCoroutineTask
object ConfigManagerClient {
val JSON = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
fun init() {
Networking.s2cHeroSettingsPacket.receiveOnClient { packet, context ->
mcCoroutineTask(sync = true, client = true) {
val decoded = JSON.decodeFromString<List<Hero.HeroJson>>(packet)
for (heroJson in decoded) {
logger.info("Loading HeroJson ${heroJson.internalKey}")
HeroManager.getHero(heroJson.internalKey)?.load(heroJson)
}
}
}
}
}
@@ -0,0 +1,17 @@
package gg.norisk.heroes.client.events
import net.silkmc.silk.core.event.Cancellable
import net.silkmc.silk.core.event.Event
import net.silkmc.silk.core.event.EventScopeProperty
object ClientEvents {
data class CameraClipToSpaceEvent(var value: Double)
val cameraClipToSpaceEvent = Event.onlySync<CameraClipToSpaceEvent>()
class PreHotbarScrollEvent: Cancellable {
override val isCancelled: EventScopeProperty<Boolean> = EventScopeProperty(false)
}
val preHotbarScrollEvent = Event.onlySync<PreHotbarScrollEvent>()
}
@@ -0,0 +1,113 @@
package gg.norisk.heroes.client.hero.ability
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.ability.AbilityPacketDescription
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility
import gg.norisk.heroes.common.hero.ability.implementation.PressAbility
import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility
import gg.norisk.heroes.common.hero.getHero
import gg.norisk.utils.events.KeyEvents
import gg.norisk.utils.events.MouseEvents
import net.minecraft.client.MinecraftClient
import net.minecraft.entity.player.PlayerEntity
import net.silkmc.silk.core.event.EventPriority
object AbilityKeyBindManager {
fun initializeKeyBindListeners() {
MouseEvents.mouseClickEvent.listen(EventPriority.FIRST) { event ->
if (MinecraftClient.getInstance().currentScreen != null) return@listen
val player = MinecraftClient.getInstance().player ?: return@listen
val hero = MinecraftClient.getInstance().player?.getHero() ?: return@listen
hero.getUsableAbilities(player).filter { it.keyBind?.matchesMouse(event.key.code) ?: false }
.sortedByDescending { it.condition != null }.forEach { ability ->
val isConditionMet =
if (ability.condition == null) true else ability.condition?.invoke(player) == true
if (isConditionMet && handleAbility(player, hero, ability, event.pressed, event.pressed)) {
event.isCancelled.set(true)
return@listen
}
}
}
KeyEvents.keyEvent.listen(EventPriority.FIRST) { event ->
if (MinecraftClient.getInstance().currentScreen != null) return@listen
val player = MinecraftClient.getInstance().player ?: return@listen
val hero = MinecraftClient.getInstance().player?.getHero() ?: return@listen
hero.getUsableAbilities(player).filter { it.keyBind?.matchesKey(event.key, event.scanCode) ?: false }
.sortedByDescending { it.condition != null }.forEach { ability ->
val isConditionMet =
if (ability.condition == null) true else ability.condition?.invoke(player) == true
if (isConditionMet && handleAbility(
player,
hero,
ability,
event.isClicked(),
event.isHold()
)
) {
event.isCancelled.set(true)
return@listen
}
}
}
}
/* TODO FUNKTIONIERT DAS GUT?
fun initializeKeyBind(ability: AbstractAbility<*>) {
logger.info("Initialize Keybind for Ability ${ability.internalKey}")
val keyBind = ability.keyBind ?: return
mouseClickEvent.listen { event ->
if (keyBind.matchesMouse(event.key.code) && canUseAbility(ability)) {
handleAbility(ability, event.pressed)
}
}
keyEvent.listen { event ->
if (event.isHold()) return@listen
if (keyBind.matchesKey(event.key, event.scanCode) && canUseAbility(ability)) {
handleAbility(ability, event.isClicked())
}
}
}
*/
private fun handleAbility(
player: PlayerEntity,
hero: Hero,
ability: AbstractAbility<*>,
pressed: Boolean,
hold: Boolean,
): Boolean {
when (ability) {
is PressAbility -> {
if (!pressed) return false
return AbilityManagerClient.useAbility(player, hero, ability, AbilityPacketDescription.Use())
}
is HoldAbility -> {
if (pressed) {
if (AbilityManagerClient.isUsingAbility(player, ability)) return false
return AbilityManagerClient.startAbility(player, hero, ability)
} else if (hold) {
return false
} else {
if (!AbilityManagerClient.isUsingAbility(player, ability)) return false
return AbilityManagerClient.endAbility(player, hero, ability)
}
}
is ToggleAbility -> {
if (pressed) return false
return if (!AbilityManagerClient.isUsingAbility(player, ability)) {
AbilityManagerClient.startAbility(player, hero, ability)
} else {
AbilityManagerClient.endAbility(player, hero, ability)
}
}
else -> {
return false
}
}
}
}
@@ -0,0 +1,168 @@
package gg.norisk.heroes.client.hero.ability
import gg.norisk.datatracker.entity.syncedValueChangeEvent
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.HeroManager
import gg.norisk.heroes.common.hero.ability.*
import gg.norisk.heroes.common.hero.ability.implementation.Ability
import gg.norisk.heroes.common.hero.ability.implementation.PressAbility
import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility
import gg.norisk.heroes.common.hero.getHero
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.heroes.common.networking.Networking.s2cAbilityPacket
import gg.norisk.heroes.common.networking.Networking.s2cCooldownPacket
import gg.norisk.utils.DevUtils.uniqueId
import net.minecraft.client.MinecraftClient
import net.minecraft.client.network.AbstractClientPlayerEntity
import net.minecraft.entity.player.PlayerEntity
import net.silkmc.silk.core.task.mcCoroutineTask
import java.util.*
object AbilityManagerClient : IAbilityManager {
private val abilitiesInUse = hashMapOf<UUID, AbstractAbility<*>>()
override fun init() {
//cleanup
syncedValueChangeEvent.listen { event ->
if (event.key != HeroManager.HERO_KEY) return@listen
val player = event.entity as? AbstractClientPlayerEntity? ?: return@listen
if (player == MinecraftClient.getInstance().player) {
(event.oldValue as? Hero?)?.abilities?.forEach { (name, ability) -> ability.onDisable(player) }
abilitiesInUse.remove(player.uniqueId)
HeroManager.registeredHeroes.values.forEach {
it.abilities.values.forEach { ability ->
ability.removeCooldown(player)
}
}
player.getHero()?.abilities?.forEach { (name, ability) -> ability.onEnable(player) }
}
}
s2cCooldownPacket.receiveOnClient { packet, context ->
mcCoroutineTask(sync = true, client = true) {
val player =
context.client.world?.getEntityById(packet.entityId) as? PlayerEntity? ?: return@mcCoroutineTask
val hero = HeroManager.getHero(packet.heroKey) ?: return@mcCoroutineTask
val ability = hero.abilities[packet.abilityKey] ?: return@mcCoroutineTask
ability.setCooldown(packet, player)
}
}
s2cAbilityPacket.receiveOnClient { packet, context ->
runCatching {
val player = context.client.player ?: return@receiveOnClient
val heroPlayer = context.client.world?.players?.firstOrNull { it.uuid == packet.playerUuid }
?: return@receiveOnClient
val ability = getAbilityFromAbilityPacket(packet) ?: return@receiveOnClient
val description = packet.description
val isOwnPacket = heroPlayer.uuid == player.uuid
val abilityScope = AbilityScope(heroPlayer)
when (ability) {
is Ability,
is PressAbility -> {
ability.onStart(player, abilityScope)
/*val callbacks = ability.internalCallbacks as AbstractAbility.ReceiveCallbacks
callbacks.handleAllClients?.invoke(heroPlayer, player, description)
if (isOwnPacket) {
callbacks.handleOwnClient?.invoke(player, description)
} else {
callbacks.handleOtherClients?.invoke(heroPlayer, player, description)
}*/
}
is ToggleAbility -> {
val callbacks = when (description) {
is AbilityPacketDescription.Start -> {
abilitiesInUse[packet.playerUuid] = ability
logger.info("Start")
ability.onStart(player, abilityScope)
//ability.internalCallbacks.START
}
is AbilityPacketDescription.Use -> {
ability.onUse(player)
//ability.internalCallbacks.USE
}
is AbilityPacketDescription.End -> {
abilitiesInUse.remove(packet.playerUuid)
ability.onEnd(player, ToggleAbility.AbilityEndInformation())
//(ability).internalCallbacks.END
}
}
//callbacks.handleAllClients?.invoke(heroPlayer, player, description)
if (isOwnPacket) {
//callbacks.handleOwnClient?.invoke(player, description)
} else {
//callbacks.handleOtherClients?.invoke(heroPlayer, player, description)
}
}
else -> error("Received an unknown Ability?")
}
}
}
}
override fun isUsingAbility(player: PlayerEntity, ability: AbstractAbility<*>): Boolean {
return abilitiesInUse[player.uuid]?.internalKey == ability.internalKey
}
override fun registerAbility(ability: AbstractAbility<*>) {
// REMOVED AbilityKeyBindManager.initializeKeyBind(ability)
}
override fun useAbility(
player: PlayerEntity,
hero: Hero,
ability: AbstractAbility<*>,
description: AbilityPacketDescription.Use
): Boolean {
if (ability.hasCooldown(player)) {
return false
} else {
val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, description)
Networking.c2sAbilityPacket.send(packet)
return true
}
}
override fun useAbility(
player: PlayerEntity,
ability: AbstractAbility<*>,
description: AbilityPacketDescription.Use
) {
val hero = player.getHero() ?: return
//player.sendDebugMessage("Sending Start Use $ability".literal)
val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, description)
Networking.c2sAbilityPacket.send(packet)
}
fun startAbility(player: PlayerEntity, hero: Hero, ability: ToggleAbility): Boolean {
if (ability.hasCooldown(player)) {
return false
} else {
//player.sendDebugMessage("Sending Start Ability $ability".literal)
val packet =
AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, AbilityPacketDescription.Start)
Networking.c2sAbilityPacket.send(packet)
return true
}
}
fun endAbility(player: PlayerEntity, hero: Hero, ability: ToggleAbility): Boolean {
//player.sendDebugMessage("Sending End Ability $ability".literal)
val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, AbilityPacketDescription.End)
Networking.c2sAbilityPacket.send(packet)
return true
}
private fun getAbilityFromAbilityPacket(packet: AbilityPacket<out AbilityPacketDescription>): AbstractAbility<*>? {
val hero = HeroManager.getHero(packet.heroKey) ?: return null
val ability = hero.abilities[packet.abilityKey]
return ability
}
}
@@ -0,0 +1,57 @@
package gg.norisk.heroes.client.networking
import gg.norisk.heroes.common.events.mouseScrollEvent
import gg.norisk.heroes.common.networking.Networking.mousePacket
import gg.norisk.heroes.common.networking.Networking.mouseScrollPacket
import gg.norisk.heroes.common.networking.dto.MouseAction
import gg.norisk.heroes.common.networking.dto.MousePacket
import gg.norisk.heroes.common.networking.dto.MouseType
import gg.norisk.utils.events.MouseEvents.mouseClickEvent
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.minecraft.client.MinecraftClient
object MouseListener {
fun initClient() {
mouseScrollEvent.listen {
MinecraftClient.getInstance().player ?: return@listen
mouseScrollPacket.send(it.vertical > 0)
}
mouseClickEvent.listen {
MinecraftClient.getInstance().player ?: return@listen
if (MinecraftClient.getInstance().options.attackKey.matchesMouse(it.key.code)) {
mousePacket.send(
MousePacket(
MouseType.LEFT,
if (it.pressed) MouseAction.CLICK else MouseAction.RELEASE
)
)
} else if (MinecraftClient.getInstance().options.useKey.matchesMouse(it.key.code)) {
mousePacket.send(
MousePacket(
MouseType.RIGHT,
if (it.pressed) MouseAction.CLICK else MouseAction.RELEASE
)
)
} else if (MinecraftClient.getInstance().options.pickItemKey.matchesMouse(it.key.code)) {
mousePacket.send(
MousePacket(
MouseType.MIDDLE,
if (it.pressed) MouseAction.CLICK else MouseAction.RELEASE
)
)
}
}
ClientTickEvents.END_CLIENT_TICK.register {
MinecraftClient.getInstance().player ?: return@register
if (MinecraftClient.getInstance().options.attackKey.isPressed) {
mousePacket.send(MousePacket(MouseType.LEFT, MouseAction.HOLD))
}
if (MinecraftClient.getInstance().options.useKey.isPressed) {
mousePacket.send(MousePacket(MouseType.RIGHT, MouseAction.HOLD))
}
if (MinecraftClient.getInstance().options.pickItemKey.isPressed) {
mousePacket.send(MousePacket(MouseType.MIDDLE, MouseAction.HOLD))
}
}
}
}
@@ -0,0 +1,86 @@
package gg.norisk.heroes.client.option
import gg.norisk.heroes.common.hero.getHero
import gg.norisk.utils.events.KeyEvents
import net.fabricmc.api.EnvType
import net.fabricmc.api.Environment
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.MinecraftClient
import net.minecraft.client.option.KeyBinding
import net.minecraft.client.util.InputUtil
import org.lwjgl.glfw.GLFW
object HeroKeyBindings {
val firstKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null
else KeyBindingHelper.registerKeyBinding(
KeyBinding(
"key.heroes.first",
InputUtil.Type.KEYSYM,
GLFW.GLFW_KEY_X,
"category.heroes.abilities"
)
)
val secondKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null
else KeyBindingHelper.registerKeyBinding(
KeyBinding(
"key.heroes.second", // The translation key of the keybinding's name
InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse.
GLFW.GLFW_KEY_V, // The keycode of the key
"category.heroes.abilities" // The translation key of the keybinding's category.
)
)
val thirdKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null
else KeyBindingHelper.registerKeyBinding(
KeyBinding(
"key.heroes.third", // The translation key of the keybinding's name
InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse.
GLFW.GLFW_KEY_G, // The keycode of the key
"category.heroes.abilities" // The translation key of the keybinding's category.
)
)
val fourthKeyBinding = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null
else KeyBindingHelper.registerKeyBinding(
KeyBinding(
"key.heroes.fourth", // The translation key of the keybinding's name
InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse.
GLFW.GLFW_KEY_H, // The keycode of the key
"category.heroes.abilities" // The translation key of the keybinding's category.
)
)
val fifthKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null
else KeyBindingHelper.registerKeyBinding(
KeyBinding(
"key.heroes.fifth", // The translation key of the keybinding's name
InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse.
GLFW.GLFW_KEY_Z, // The keycode of the key
"category.heroes.abilities" // The translation key of the keybinding's category.
)
)
val pickItemKeyBinding by lazy {
if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null
else MinecraftClient.getInstance().options.pickItemKey
}
val heroKeyBindings by lazy { listOf(firstKeyBind, secondKeyBind, thirdKeyBind, fourthKeyBinding, fifthKeyBind) }
//TODO das maybe als config damit wir lvie updaten können?
val blacklist = mutableSetOf("key.voice_chat", "key.voice_chat_group", "key.hide_icons")
@Environment(EnvType.CLIENT)
fun initClient() {
KeyEvents.keyBindingOnPressEvent.listen { event ->
val player = MinecraftClient.getInstance().player ?: return@listen
if (player.getHero() == null) return@listen
if (blacklist.contains(event.keybinding.translationKey)) {
if (heroKeyBindings.any { it?.equals(event.keybinding) == true}) {
event.isCancelled.set(true)
}
}
}
}
}
@@ -0,0 +1,87 @@
package gg.norisk.heroes.client.renderer
import gg.norisk.heroes.common.utils.toVec
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.RenderLayer
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.render.WorldRenderer
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Box
object BlockOutlineRenderer {
fun drawBlockBox(
matrixStack: MatrixStack,
vertexConsumerProvider: VertexConsumerProvider,
blockPos: BlockPos,
f: Float,
g: Float,
h: Float,
i: Float
) {
drawBox(matrixStack, vertexConsumerProvider, blockPos, blockPos.add(1, 1, 1), f, g, h, i)
}
fun drawBox(
matrixStack: MatrixStack,
vertexConsumerProvider: VertexConsumerProvider,
blockPos: BlockPos,
blockPos2: BlockPos,
f: Float,
g: Float,
h: Float,
i: Float
) {
val camera = MinecraftClient.getInstance().gameRenderer.camera
if (camera.isReady) {
val vec3d = camera.pos.negate()
val box = Box.from(blockPos.toVec()).offset(vec3d)
drawBox(matrixStack, vertexConsumerProvider, box, f, g, h, i)
}
}
fun drawBox(
matrixStack: MatrixStack,
vertexConsumerProvider: VertexConsumerProvider,
box: Box,
f: Float,
g: Float,
h: Float,
i: Float
) {
drawBox(
matrixStack,
vertexConsumerProvider,
box.minX,
box.minY,
box.minZ,
box.maxX,
box.maxY,
box.maxZ,
f,
g,
h,
i
)
}
fun drawBox(
matrixStack: MatrixStack,
vertexConsumerProvider: VertexConsumerProvider,
d: Double,
e: Double,
f: Double,
g: Double,
h: Double,
i: Double,
j: Float,
k: Float,
l: Float,
m: Float
) {
val vertexConsumer = vertexConsumerProvider.getBuffer(RenderLayer.getDebugFilledBox())
WorldRenderer.renderFilledBox(matrixStack, vertexConsumer, d, e, f, g, h, i, j, k, l, m)
}
}
@@ -0,0 +1,105 @@
package gg.norisk.heroes.client.renderer
import com.google.common.util.concurrent.AtomicDouble
import gg.norisk.heroes.common.networking.CameraShakeEvent
import gg.norisk.heroes.common.networking.cameraShakePacket
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
import net.minecraft.client.MinecraftClient
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.util.Util
import kotlin.random.Random
//Credits to https://github.com/LoganDark/fabric-camera-shake
object CameraShaker {
var avgX: Double = 0.0
var avgY: Double = 0.0
private var smooth: Int = 3
private var pastI: Int = 0
private val pastX: DoubleArray = DoubleArray(smooth)
private val pastY: DoubleArray = DoubleArray(smooth)
private val events: MutableSet<CameraShakeEvent> = HashSet()
private val providers: MutableSet<CameraShakeProvider> = HashSet()
private var eventInceptions = Object2LongOpenHashMap<CameraShakeEvent>()
fun newFrame() {
val magnitude: Double = getCameraShakeMagnitude(MinecraftClient.getInstance().player)
val x: Double = (Random.nextDouble() - .5) * magnitude
val y: Double = (Random.nextDouble() - .5) * magnitude
pastX[pastI] = x
pastY[pastI++] = y
pastI %= smooth
calculateAvg()
}
private fun calculateAvg() {
avgX = .0
avgY = .0
for (i in 0 until smooth) {
avgX += pastX[i]
avgY += pastY[i]
}
avgX /= smooth.toDouble()
avgY /= smooth.toDouble()
}
interface CameraShakeProvider {
fun getCameraShakeMagnitude(player: ClientPlayerEntity?): Double
}
fun initClient() {
cameraShakePacket.receiveOnClient { packet, context ->
addEvent(packet)
}
}
private fun getCameraShakeMagnitude(player: ClientPlayerEntity?): Double {
val magnitude = AtomicDouble()
val eventIterator: MutableIterator<CameraShakeEvent> = events.iterator()
eventIterator.forEachRemaining { event: CameraShakeEvent ->
val t: Double
try {
t = getTime(event)
} catch (e: IllegalArgumentException) {
eventIterator.remove()
return@forEachRemaining
}
if (!event.isValid(t)) {
eventIterator.remove()
onEventRemoved(event)
} else {
magnitude.addAndGet(event.getCameraShakeMagnitude(t))
}
}
for (provider in providers) {
magnitude.addAndGet(provider.getCameraShakeMagnitude(player))
}
return magnitude.get()
}
private fun <E : CameraShakeEvent> onEventRemoved(event: E) {
eventInceptions.removeLong(event)
}
private fun <E : CameraShakeEvent?> onEventAdded(event: E) {
eventInceptions.put(event, Util.getMeasuringTimeNano())
}
private fun getTime(event: CameraShakeEvent): Double {
require(eventInceptions.containsKey(event)) { "Passed event was not added to this CameraShakeManager" }
val then: Long = eventInceptions.getLong(event)
val now = Util.getMeasuringTimeNano()
val delta = now - then
return delta.toDouble() / 1000000000.0
}
fun <E : CameraShakeEvent> addEvent(event: E): E? {
if (events.add(event)) {
onEventAdded(event)
return event
}
return null
}
}
@@ -0,0 +1,109 @@
package gg.norisk.heroes.client.renderer
import gg.norisk.heroes.common.hero.getHero
import gg.norisk.heroes.common.hero.utils.ColorUtils
import gg.norisk.ui.api.value.key
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.render.RenderLayer
import net.minecraft.client.render.RenderTickCounter
import net.minecraft.text.Text
import net.minecraft.util.Colors
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
import net.silkmc.silk.core.world.pos.Pos2i
import java.awt.Color
object KeyBindHud {
fun init() {
HudRenderCallback.EVENT.register(::render)
}
fun render(drawContext: DrawContext, tickCounter: RenderTickCounter) {
if (MinecraftClient.getInstance().debugHud.shouldShowDebugHud()) return
val player = MinecraftClient.getInstance().player ?: return
val hero = player.getHero() ?: return
val offset = 2
val scale = 0.75f
drawContext.matrices.push()
drawContext.matrices.scale(scale, scale, scale)
hero.getUsableAbilities(player).map { ability ->
val keyBind = ability.keyBind
var text = literalText {
text {
//if (keyBind.condition != null) text("${keyBind.condition.hudText} + ")
val deactivatedColor = 0x4A4A4A
text(
keyBind?.boundKeyLocalizedText ?: keyBind?.defaultKey?.localizedText
?: ability.getCustomActivation()
) {
color = if (ability.hasCooldown(player)) {
deactivatedColor
} else {
hero.color
}
}
if (ability.condition != null) {
text(" + ")
text(Text.translatable("heroes.ability.condition.short.${ability.internalKey}")) {
color = if (ability.condition?.invoke(player) == true || ability.condition == null) {
hero.color
} else {
deactivatedColor
}
}
}
}
text(" - ") { color = 0x919191 }
text(ability.name)
}
ability.getCooldown(player)?.let { cooldownInfo ->
text = literalText {
text(text)
cooldownInfo.durationString?.let { extension ->
text(" ")
text(extension) { color = ColorUtils.contrast(0x248223) }
}
}
}
text to ability
}.sortedByDescending { MinecraftClient.getInstance().textRenderer.getWidth(it.first) }
.forEachIndexed { index, (text, ability) ->
val pos = Pos2i(5, 5 + (text.height + offset * 2) * index)
drawContext.fill(
RenderLayer.getGuiOverlay(),
pos.x - offset,
pos.z - offset,
pos.x + text.width + offset,
pos.z + text.height + offset,
-1873784752
)
drawContext.drawText(
MinecraftClient.getInstance().textRenderer, literalText {
if (!ability.hasUnlocked(player)) {
text(text.string)
strikethrough = true
color = Colors.LIGHT_GRAY
} else {
text(text)
}
}, pos.x, pos.z, 14737632, true
)
}
drawContext.matrices.pop()
}
val Text.width
get() = MinecraftClient.getInstance().textRenderer.getWidth(this)
val Text.height
get() = MinecraftClient.getInstance().textRenderer.fontHeight
}
@@ -0,0 +1,30 @@
package gg.norisk.heroes.client.renderer
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.gui.DrawContext
import net.minecraft.util.Identifier
object RenderUtils {
fun renderOverlay(drawContext: DrawContext, identifier: Identifier, f: Float) {
RenderSystem.disableDepthTest()
RenderSystem.depthMask(false)
RenderSystem.enableBlend()
drawContext.setShaderColor(1.0f, 1.0f, 1.0f, f)
drawContext.drawTexture(
identifier,
0,
0,
-90,
0.0f,
0.0f,
drawContext.scaledWindowWidth,
drawContext.scaledWindowHeight,
drawContext.scaledWindowWidth,
drawContext.scaledWindowHeight
)
RenderSystem.disableBlend()
RenderSystem.depthMask(true)
RenderSystem.enableDepthTest()
drawContext.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f)
}
}
@@ -0,0 +1,153 @@
package gg.norisk.heroes.client.renderer
import com.mojang.blaze3d.systems.RenderSystem
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.getHero
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.MinecraftClient
import net.minecraft.client.network.AbstractClientPlayerEntity
import net.minecraft.client.texture.NativeImage
import net.minecraft.client.texture.NativeImageBackedTexture
import net.minecraft.util.Identifier
import java.io.File
import java.io.IOException
import kotlin.jvm.optionals.getOrNull
object SkinUtils {
fun initClient() {
if (!FabricLoader.getInstance().isDevelopmentEnvironment) return
}
fun applyOverlay(baseSkinId: Identifier, overlayId: Identifier, hero: Hero) {
val result = combineSkins(baseSkinId, overlayId)
if (FabricLoader.getInstance().isDevelopmentEnvironment) {
logger.info("Merged Skin $baseSkinId $overlayId: $result")
val folder = File(FabricLoader.getInstance().configDir.toFile(), "aang").apply { mkdirs() }
//MinecraftClient.getInstance().textureManager.registerTexture()
result?.writeTo(File(folder, "${baseSkinId.path}_overlay.png"))
}
if (result != null) {
MinecraftClient.getInstance().submit {
MinecraftClient.getInstance().textureManager.registerTexture(
baseSkinId.toOverlaySkin(hero.internalKey.lowercase()),
NativeImageBackedTexture(result)
)
}
}
}
fun Identifier.toOverlaySkin(id: String): Identifier {
return Identifier.of(this.namespace, this.path + "_$id")
}
fun redirectCombinedSkin(original: Identifier, player: AbstractClientPlayerEntity?): Identifier {
val hero = player?.getHero()
val skin = hero?.overlaySkin
if (hero != null && skin != null) {
val mergedSkin = original.toOverlaySkin(hero.internalKey.lowercase())
if (MinecraftClient.getInstance().textureManager.getOrDefault(mergedSkin, null) != null) {
return mergedSkin
} else {
applyOverlay(original, skin, hero)
return original
}
} else {
return original
}
}
fun extractTextureToNativeImage(glId: Int, width: Int, height: Int): NativeImage {
// Erstelle ein NativeImage mit der passenden Breite, Höhe und Format
val nativeImage = NativeImage(NativeImage.Format.RGBA, width, height, false)
// Binde die Textur mit der OpenGL-ID
RenderSystem.bindTexture(glId)
// Lade die Textur-Pixel-Daten aus OpenGL in das NativeImage
nativeImage.loadFromTextureImage(0, false) // i = 0 für Mipmap Level 0, bl = false für keine Alpha-Korrektur
return nativeImage // Das NativeImage enthält nun die Texturdaten
}
/**
* Kombiniert zwei Skins, wobei der zweite Skin alles ersetzt, was nicht transparent ist,
* und gibt das resultierende Bild als NativeImage zurück.
*
* @param baseSkin Der Identifier für das Basisbild (der erste Skin).
* @param overlaySkin Der Identifier für das Overlay-Bild (der zweite Skin).
* @return Das kombinierte Bild als NativeImage.
*/
fun combineSkins(baseSkin: Identifier, overlaySkin: Identifier): NativeImage? {
return try {
// Lade die Basis- und Overlay-Skins als NativeImage
val baseImageTexture = MinecraftClient.getInstance().textureManager.getOrDefault(baseSkin, null)
val overlayImage = loadSkinAsNativeImage(overlaySkin)
// Falls einer der beiden Skins nicht geladen werden konnte
if (baseImageTexture == null || overlayImage == null) {
return null
}
//ich hoffe das macht nichts kapuut lol
val baseImage: NativeImage = extractTextureToNativeImage(baseImageTexture.glId, 64, 64)
// Überprüfen, ob beide Bilder die gleiche Größe haben
if (baseImage.width != overlayImage.width || baseImage.height != overlayImage.height) {
return null
}
// Erstelle ein neues NativeImage mit der gleichen Größe wie die Basis
val combinedImage = NativeImage(baseImage.width, baseImage.height, true)
// Durchlaufen der Pixel und anwenden des Overlays auf das neue Bild
for (x in 0 until baseImage.width) {
for (y in 0 until baseImage.height) {
val baseColor = baseImage.getColor(x, y)
val overlayColor = overlayImage.getColor(x, y)
val alpha = (overlayColor shr 24) and 0xFF // Alpha-Wert extrahieren
// Wenn das Overlay-Pixel nicht transparent ist, kopiere es ins kombinierte Bild
if (alpha > 0) {
combinedImage.setColor(x, y, overlayColor)
} else {
// Andernfalls kopiere das Basis-Pixel
combinedImage.setColor(x, y, baseColor)
}
}
}
// Freigeben der Basis- und Overlay-Bilder, da sie nicht mehr benötigt werden
baseImage.close()
overlayImage.close()
// Gib das kombinierte Bild zurück
combinedImage
} catch (e: IOException) {
e.printStackTrace()
null
}
}
/**
* Lädt einen Skin von einem Identifier als NativeImage.
*
* @param skinIdentifier Der Identifier des Skins.
* @return Das NativeImage des Skins, oder null falls das Bild nicht geladen werden konnte.
*/
fun loadSkinAsNativeImage(skinIdentifier: Identifier): NativeImage? {
val resourceManager = MinecraftClient.getInstance().resourceManager
return try {
val resource = resourceManager.getResource(skinIdentifier).getOrNull() ?: return null
NativeImage.read(resource.inputStream)
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,83 @@
package gg.norisk.heroes.client.renderer
import com.mojang.blaze3d.systems.RenderSystem
import gg.norisk.datatracker.entity.getSyncedData
import gg.norisk.datatracker.entity.setSyncedData
import gg.norisk.heroes.common.HeroesManager.toId
import net.fabricmc.fabric.api.client.rendering.v1.CoreShaderRegistrationCallback
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gl.GlUniform
import net.minecraft.client.gl.ShaderProgram
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.render.*
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.util.math.MathHelper
import org.lwjgl.opengl.GL11
import kotlin.math.max
import kotlin.math.min
object Speedlines {
var lerpedSpeed: Double = 0.0
lateinit var edge: GlUniform
lateinit var speedlinesRenderTypeProgram: ShaderProgram
private const val SPEEDLINES_KEY = "speedlines"
var PlayerEntity.showSpeedlines: Boolean
get() = this.getSyncedData<Boolean>(SPEEDLINES_KEY) == true
set(value) {
this.setSyncedData(SPEEDLINES_KEY, value)
}
fun initClient() {
CoreShaderRegistrationCallback.EVENT.register(CoreShaderRegistrationCallback { context: CoreShaderRegistrationCallback.RegistrationContext ->
context.register(
"speedlines".toId(), VertexFormats.POSITION
) { shaderProgram: ShaderProgram ->
speedlinesRenderTypeProgram = shaderProgram
edge = shaderProgram.getUniform("Edge")!!
}
})
HudRenderCallback.EVENT.register(HudRenderCallback { context: DrawContext, tickCounter: RenderTickCounter ->
val player = MinecraftClient.getInstance().player ?: return@HudRenderCallback
val client = MinecraftClient.getInstance()
if (player.showSpeedlines) {
val width = client.getWindow().width.toFloat()
val height = client.getWindow().height.toFloat()
val delta = tickCounter.getTickDelta(false)
lerpedSpeed =
MathHelper.lerp((delta * 0.05f).toDouble(), lerpedSpeed, client.player!!.velocity.length())
var speed = max(0.0, (lerpedSpeed - 0.2) / 2f)
speed = min(speed, 0.2)
edge.set((0.5f - speed).toFloat())
val positionMatrix = context.matrices.peek().positionMatrix
val tessellator = Tessellator.getInstance().begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION)
tessellator.vertex(positionMatrix, 0f, height, 0f)
tessellator.vertex(positionMatrix, 0f, 0f, 0f)
tessellator.vertex(positionMatrix, width, 0f, 0f)
tessellator.vertex(positionMatrix, width, height, 0f)
RenderSystem.setShader { speedlinesRenderTypeProgram }
setupRender()
BufferRenderer.drawWithGlobalProgram(tessellator.end())
endRender()
}
})
}
private fun setupRender() {
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
RenderSystem.disableCull()
RenderSystem.depthFunc(GL11.GL_ALWAYS)
}
private fun endRender() {
RenderSystem.disableBlend()
}
}
@@ -0,0 +1,94 @@
package gg.norisk.heroes.client.ui
import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen
import gg.norisk.heroes.client.ui.skilltree.HeroSelectorScreenV2
import gg.norisk.heroes.client.ui.skilltree.SkillTreeScreen
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.HeroManager
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket
import gg.norisk.utils.OldAnimation
import me.cortex.nvidium.Nvidium
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.MinecraftClient
import net.minecraft.util.math.MathHelper
import net.silkmc.silk.commands.clientCommand
import net.silkmc.silk.core.kotlin.ticks
import net.silkmc.silk.core.task.mcCoroutineTask
import org.joml.Matrix4f
import org.joml.Quaternionf
import kotlin.math.max
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
object OrthoCamera {
val isEnabled get() = MinecraftClient.getInstance().currentScreen is HeroSelectorScreen || MinecraftClient.getInstance().currentScreen is HeroSelectorScreenV2
var yawAnimation = OldAnimation(0f, 360f, 2.minutes.toJavaDuration())
fun initClient() {
Networking.s2cHeroSelectorPacket.receiveOnClient { packet, context ->
if (packet.isActive) {
openHeroSelectorScreen(packet.heroes.mapNotNull { HeroManager.getHero(it) }, packet)
} else {
mcCoroutineTask(sync = true, client = true) {
if (isEnabled) {
if (FabricLoader.getInstance().isModLoaded("nvidium")) {
Nvidium.FORCE_DISABLE = false
MinecraftClient.getInstance()?.worldRenderer?.reload()
}
MinecraftClient.getInstance().setScreen(null)
}
}
}
}
clientCommand("heroselector") {
runs {
mcCoroutineTask(delay = 1.ticks, sync = true, client = true) {
MinecraftClient.getInstance().setScreen(HeroSelectorScreen(HeroManager.registeredHeroes.values.toList(), false))
}
}
}
clientCommand("skilltree") {
runs {
mcCoroutineTask(sync = true, client = true, delay = 1.ticks) {
MinecraftClient.getInstance().setScreen(SkillTreeScreen())
}
}
}
}
fun openHeroSelectorScreen(
heroes: List<Hero> = HeroManager.registeredHeroes.values.toList(),
packet: HeroSelectorPacket
) {
mcCoroutineTask(delay = 1.ticks, sync = true, client = true) {
MinecraftClient.getInstance().setScreen(HeroSelectorScreenV2(heroes, packet.isKitEditorEnabled))
}
}
fun createOrthoMatrix(delta: Float, minScale: Float): Matrix4f {
val client: MinecraftClient = MinecraftClient.getInstance()
val scale = 100f
val width = max(minScale, scale * client.window.framebufferWidth / client.window.framebufferHeight)
val height = max(minScale, scale)
return Matrix4f().setOrtho(
-width, width,
-height, height,
-1000.0F, 1000.0f
)
}
fun handlePitch(quaternion: Quaternionf, tickDelta: Float): Float {
return 30f * MathHelper.RADIANS_PER_DEGREE
}
fun handleYaw(quaternion: Quaternionf, tickDelta: Float): Float {
if (yawAnimation.isDone) {
yawAnimation.reset()
}
return yawAnimation.get() * MathHelper.RADIANS_PER_DEGREE
}
}
@@ -0,0 +1,53 @@
package gg.norisk.heroes.client.ui.components
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.ui.components.ScalableButtonComponent
import io.wispforest.owo.ui.component.ButtonComponent
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.OwoUIDrawContext
import io.wispforest.owo.ui.core.Sizing
import net.minecraft.client.MinecraftClient
import net.silkmc.silk.core.text.literal
import java.util.*
class AbilitiesComponent(
val hero: Hero,
val uuid: UUID = MinecraftClient.getInstance().player!!.uuid,
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
val mainWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content())
val buttonWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content())
init {
for (ability in hero.abilities.values) {
buttonWrapper.child(ScalableButtonComponent(ability.name.literal, 0.8f) {
onAbilityButtonClick(it, ability)
})
}
child(buttonWrapper)
child(mainWrapper)
buttonWrapper.children().filterIsInstance<ButtonComponent>().first().onPress()
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
for (child in mainWrapper.children()) {
val width = buttonWrapper.fullSize().width
child.horizontalSizing(Sizing.fixed(width))
}
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
private fun onAbilityButtonClick(it: ButtonComponent, ability: AbstractAbility<*>) {
buttonWrapper.children().filterIsInstance<ButtonComponent>().forEach { button ->
button.active(true)
}
it.active(false)
mainWrapper.clearChildren()
mainWrapper.child(AbilityComponent(ability))
}
}
@@ -0,0 +1,208 @@
package gg.norisk.heroes.client.ui.components
import gg.norisk.heroes.common.ability.CooldownProperty
import gg.norisk.heroes.common.ability.LevelInformation
import gg.norisk.heroes.common.ability.PlayerProperty
import gg.norisk.heroes.common.ability.SingleUseProperty
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.common.hero.ability.SkillPropertyPacket
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.ui.components.ScalableButtonComponent
import gg.norisk.ui.components.ScalableLabelComponent
import io.wispforest.owo.ui.component.ButtonComponent
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.*
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.RenderLayer
import net.minecraft.text.Text
import net.silkmc.silk.core.task.mcCoroutineTask
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
import java.awt.Color
import java.util.*
class AbilityComponent(
val ability: AbstractAbility<*>,
val uuid: UUID = MinecraftClient.getInstance().player!!.uuid,
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.HORIZONTAL) {
val abilityDescription = ScalableLabelComponent(ability.description, 0.5f).apply {
shadow(true)
}
val leftWrapper = Containers.verticalFlow(Sizing.fill(45), Sizing.content())
init {
surface(Surface.VANILLA_TRANSLUCENT)
padding(Insets.of(5))
leftWrapper.apply {
child(ScalableLabelComponent(literalText {
text(ability.name.literal)
underline = true
bold = true
}).apply {
shadow(true)
})
child(abilityDescription)
gap(3)
}
child(leftWrapper)
gap(2)
horizontalAlignment(HorizontalAlignment.LEFT)
child(Containers.verticalFlow(Sizing.fill(55), Sizing.content()).apply {
//debug()
gap(3)
for (property in ability.getAllProperties()) {
if (property is SingleUseProperty) continue
child(PropertyComponent(property))
}
})
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
abilityDescription.maxWidth(leftWrapper.fullSize().width * 2)
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
private inner class PropertyComponent(
val property: PlayerProperty<*>,
horizontalSizing: Sizing = Sizing.fill(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
val progressBar = Containers.horizontalFlow(Sizing.fill(83), Sizing.fixed(3))
var progressColor: Color = Color.GREEN
val title = ScalableLabelComponent(literalText {
text(Text.translatable(property.name)) {
bold = true
}
text(":")
}, 0.75f).apply {
shadow(true)
}
val valueLabel = ScalableLabelComponent(getValueText(property.getValue(uuid)), 0.75f).apply {
shadow(true)
}
val levelLabel = ScalableLabelComponent(getLevelText(property.getLevelInfo(uuid)), 0.5f).apply {
shadow(true)
}
val skillButton = ScalableButtonComponent("+".literal, 0.75f, ::onSkill).apply {
sizing(Sizing.fixed(15))
}
init {
//debug()
child(Containers.horizontalFlow(Sizing.content(), Sizing.content()).apply {
child(title)
child(valueLabel)
gap(5)
tooltip(upgradeTooltip())
//debug()
})
child(Containers.horizontalFlow(Sizing.content(), Sizing.content()).apply {
child(Containers.verticalFlow(Sizing.content(), Sizing.content()).apply {
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
child(levelLabel)
child(progressBar)
gap(3)
})
child(skillButton)
alignment(HorizontalAlignment.LEFT, VerticalAlignment.CENTER)
gap(5)
//debug()
})
gap(3)
//alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP)
}
private fun upgradeTooltip(): Text {
return literalText {
text("Upgrade") {
bold = true
}
repeat(property.maxLevel + 1) { level ->
newLine()
text("Lvl $level -> ")
text(getValueText(property.getValue(level)))
}
}
}
private fun progressTooltip(levelInformation: LevelInformation): Text {
return literalText {
text((levelInformation.xpNextLevel - levelInformation.xpTillNextLevel).toString())
text("/")
text(levelInformation.xpNextLevel.toString())
}
}
private fun onSkill(buttonComponent: ButtonComponent) {
mcCoroutineTask(client = true, sync = true) {
Networking.c2sSkillProperty.send(
SkillPropertyPacket(
ability.hero.internalKey, ability.internalKey, property.internalKey
)
)
}
}
private fun getLevelText(levelInformation: LevelInformation): Text {
return literalText {
text("Lvl ")
text(levelInformation.currentLevel.toString())
text("/")
text(levelInformation.maxLevel.toString())
}
}
private fun <T> getValueText(value: T): Text {
return literalText {
text(value.toString())
if (property is CooldownProperty) {
text("s")
}
}
}
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
val levelInfo = property.getLevelInfo(MinecraftClient.getInstance().session.uuidOrNull)
valueLabel.text(getValueText(property.getValue(uuid)))
levelLabel.text(getLevelText(levelInfo))
progressBar.tooltip(progressTooltip(levelInfo))
levelLabel.tooltip(progressTooltip(levelInfo))
val currentPercentage = levelInfo.percentageTillNextLevel
progressBar.surface { surfaceContext, component ->
val barWidth = progressBar.width() * (currentPercentage / 100.0)
surfaceContext.fill(
RenderLayer.getGui(),
component.x(),
component.y(),
(component.x() + barWidth).toInt(),
component.y() + component.height(),
0,
progressColor.rgb
)
surfaceContext.fill(
RenderLayer.getGui(),
component.x() + barWidth.toInt(),
component.y(),
(component.x() + progressBar.width()),
component.y() + component.height(),
0,
progressColor.darker().darker().withAlpha(200).rgb
)
}
}
}
fun Color.withAlpha(alpha: Int): Color {
return Color(red, green, blue, alpha)
}
}
@@ -0,0 +1,98 @@
package gg.norisk.heroes.client.ui.components
import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.ui.components.ScalableButtonComponent
import io.wispforest.owo.ui.component.ButtonComponent
import io.wispforest.owo.ui.component.Components
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.*
import io.wispforest.owo.ui.util.UISounds
import net.silkmc.silk.core.text.literal
class HeroListComponent(
val heroes: List<Hero>,
val heroSelectorScreen: HeroSelectorScreen,
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(
horizontalSizing, verticalSizing, Algorithm.VERTICAL
) {
val lockInButton = ScalableButtonComponent("LOCK IN".literal, 1f, ::onLockInButton).apply {
horizontalSizing(Sizing.fixed(100))
}
val editorButton = ScalableButtonComponent("EDITOR".literal, 1f, ::onEditorButton).apply {
horizontalSizing(Sizing.fixed(100))
}
init {
gap(5)
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
val grid = Containers.grid(Sizing.content(), Sizing.content(), 1, heroes.size)
for ((index, hero) in heroes.withIndex()) {
grid.child(HeroHeadComponent(hero), 0, index)
}
child(grid)
grid.surface(Surface.VANILLA_TRANSLUCENT)
grid.padding(Insets.of(5))
child(lockInButton)
if (heroSelectorScreen.isKitEditorEnabled) {
child(editorButton)
}
}
private fun onLockInButton(buttonComponent: ButtonComponent) {
Networking.c2sHeroSelectorPacket.send(heroSelectorScreen.hero!!.internalKey)
}
private fun onEditorButton(buttonComponent: ButtonComponent) {
Networking.c2sKitEditorRequestPacket.send(Unit)
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
lockInButton.active(heroSelectorScreen.hero != null)
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
inner class HeroHeadComponent(
val hero: Hero, horizontalSizing: Sizing = Sizing.content(), verticalSizing: Sizing = Sizing.content()
) : FlowLayout(
horizontalSizing, verticalSizing, Algorithm.HORIZONTAL
) {
init {
val l = 8
val m = 8
val heroHead = Components.texture(hero.icon, 0, 0, 64, 64, 64, 64)
//val heroHead2 = Components.texture(hero.skin, 40, l, 8, m, 64, 64)
//OVERLAY
heroHead.sizing(Sizing.fixed(32))
child(heroHead)
margins(Insets.of(2))
padding(Insets.of(2))
surface(Surface.outline(java.awt.Color.WHITE.darker().rgb))
mouseDown().subscribe { _, _, _ ->
UISounds.playButtonSound()
heroSelectorScreen.hero = hero
return@subscribe true
}
mouseEnter().subscribe {
surface(Surface.outline(Color.WHITE.argb()))
}
mouseLeave().subscribe {
surface(Surface.outline(java.awt.Color.WHITE.darker().rgb))
}
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
}
@@ -0,0 +1,131 @@
package gg.norisk.heroes.client.ui.components
import gg.norisk.heroes.client.ui.skilltree.HeroSelectorScreenV2
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.ui.components.ScalableButtonComponent
import io.wispforest.owo.ui.component.ButtonComponent
import io.wispforest.owo.ui.component.Components
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.container.OverlayContainer
import io.wispforest.owo.ui.core.*
import io.wispforest.owo.ui.util.UISounds
import net.minecraft.client.MinecraftClient
import net.silkmc.silk.core.text.literal
class HeroListComponentV2(
val heroes: List<Hero>,
val heroSelectorScreen: HeroSelectorScreenV2,
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(
horizontalSizing, verticalSizing, Algorithm.VERTICAL
) {
val lockInButton = ScalableButtonComponent("LOCK IN".literal, 1f, ::onLockInButton).apply {
horizontalSizing(Sizing.fixed(100))
}
val editorButton = ScalableButtonComponent("EDITOR".literal, 1f, ::onEditorButton).apply {
horizontalSizing(Sizing.fixed(48))
}
val lobbyButton = ScalableButtonComponent("SPEC".literal, 1f, ::onLobbyButton).apply {
horizontalSizing(Sizing.fixed(48))
}
init {
gap(5)
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
val grid = Containers.grid(Sizing.content(), Sizing.content(), 1, heroes.size)
for ((index, hero) in heroes.withIndex()) {
grid.child(HeroHeadComponent(hero), 0, index)
}
child(grid)
grid.surface(Surface.VANILLA_TRANSLUCENT)
grid.padding(Insets.of(5))
val buttonWrapper = ButtonWrapper()
child(buttonWrapper)
buttonWrapper.child(lockInButton)
if (heroSelectorScreen.isKitEditorEnabled) {
buttonWrapper.child(Containers.horizontalFlow(Sizing.content(), Sizing.content()).apply {
child(editorButton)
child(lobbyButton)
gap(5)
})
}
}
private inner class ButtonWrapper(
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(
horizontalSizing, verticalSizing, Algorithm.VERTICAL
) {
init {
gap(5)
}
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
if (heroSelectorScreen.adapter.children().any { it is OverlayContainer<*> }) {
return
}
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
private fun onLockInButton(buttonComponent: ButtonComponent) {
Networking.c2sHeroSelectorPacket.send(heroSelectorScreen.hero!!.internalKey)
}
private fun onEditorButton(buttonComponent: ButtonComponent) {
Networking.c2sKitEditorRequestPacket.send(Unit)
}
private fun onLobbyButton(buttonComponent: ButtonComponent) {
heroSelectorScreen.close()
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
lockInButton.active(heroSelectorScreen.hero != null)
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
inner class HeroHeadComponent(
val hero: Hero, horizontalSizing: Sizing = Sizing.content(), verticalSizing: Sizing = Sizing.content()
) : FlowLayout(
horizontalSizing, verticalSizing, Algorithm.HORIZONTAL
) {
init {
val l = 8
val m = 8
val heroHead = Components.texture(hero.icon, 0, 0, 64, 64, 64, 64)
//val heroHead2 = Components.texture(hero.skin, 40, l, 8, m, 64, 64)
//OVERLAY
heroHead.sizing(Sizing.fixed(32))
child(heroHead)
margins(Insets.of(2))
padding(Insets.of(2))
surface(Surface.outline(java.awt.Color.WHITE.darker().rgb))
mouseDown().subscribe { _, _, _ ->
UISounds.playButtonSound()
heroSelectorScreen.hero = hero
return@subscribe true
}
mouseEnter().subscribe {
surface(Surface.outline(Color.WHITE.argb()))
}
mouseLeave().subscribe {
surface(Surface.outline(java.awt.Color.WHITE.darker().rgb))
}
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
}
@@ -0,0 +1,113 @@
package gg.norisk.heroes.client.ui.screen
//import me.cortex.nvidium.Nvidium
import gg.norisk.heroes.client.ui.components.AbilitiesComponent
import gg.norisk.heroes.client.ui.components.HeroListComponent
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.ui.components.ScalableLabelComponent
import io.wispforest.owo.ui.base.BaseOwoScreen
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.*
import me.cortex.nvidium.Nvidium
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.MinecraftClient
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
class HeroSelectorScreen(val heroes: List<Hero>, val isKitEditorEnabled: Boolean = false) :
BaseOwoScreen<FlowLayout>() {
var hero: Hero? = null
set(value) {
heroInfoComponent?.remove()
centerLabel.remove()
if (field != value) {
field = value
if (value != null) {
heroInfoComponent = heroAbility(value)
this.uiAdapter.rootComponent.child(heroInfoComponent)
}
} else {
field = null
this.uiAdapter.rootComponent.child(centerLabel)
}
}
var heroInfoComponent: FlowLayout? = null
var centerLabel: FlowLayout = Containers.horizontalFlow(Sizing.fill(), Sizing.content()).apply {
child(ScalableLabelComponent(literalText {
text("CHOOSE YOUR HERO")
}, 3f).apply {
shadow(true)
})
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
positioning(Positioning.relative(30, 50))
}
override fun createAdapter(): OwoUIAdapter<FlowLayout> {
return OwoUIAdapter.create(this, Containers::verticalFlow);
}
private fun heroAbility(hero: Hero): FlowLayout {
val container = Containers.verticalFlow(Sizing.content(), Sizing.content())
.apply { positioning(Positioning.relative(0, 30)) }
container.child(Containers.verticalFlow(Sizing.content(), Sizing.content()).apply {
child(ScalableLabelComponent(literalText {
text(hero.name.uppercase())
bold = true
//color = Color.YELLOW.rgb
}, 2f).apply {
this.margins(Insets.of(3))
})
child(XpLabel(1f).apply {
this.margins(Insets.of(3))
})
gap(3)
})
container.gap(5)
container.child(AbilitiesComponent(hero))
return container
}
private class XpLabel(scale: Float = 1f) : ScalableLabelComponent("".literal, scale) {
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
text(literalText {
text("XP: ")
text((MinecraftClient.getInstance().player?.ffaPlayer?.xp ?: 0).toString())
})
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
override fun close() {
super.close()
if (FabricLoader.getInstance().isModLoaded("nvidium")) {
Nvidium.FORCE_DISABLE = false
this.client?.worldRenderer?.reload()
}
}
override fun build(root: FlowLayout) {
val heroList = HeroListComponent(heroes, this)
heroList.positioning(Positioning.relative(50, 90))
root.child(heroList)
if (hero == null) {
root.child(centerLabel)
}
if (FabricLoader.getInstance().isModLoaded("nvidium")) {
Nvidium.FORCE_DISABLE = true
this.client?.worldRenderer?.reload()
}
}
override fun shouldPause(): Boolean {
return false
}
override fun shouldCloseOnEsc(): Boolean {
return FabricLoader.getInstance().isDevelopmentEnvironment
}
}
@@ -0,0 +1,454 @@
package gg.norisk.heroes.client.ui.skilltree
import com.mojang.blaze3d.systems.RenderSystem
import gg.norisk.heroes.common.HeroesManager.toId
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.heroes.common.ui.ScrollContainerV2
import gg.norisk.ui.components.ScalableLabelComponent
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.*
import io.wispforest.owo.ui.util.NinePatchTexture
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.BufferRenderer
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexFormat.DrawMode
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.sound.SoundEvents
import net.minecraft.text.Text
import net.minecraft.util.Colors
import net.minecraft.util.Identifier
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
import org.joml.Matrix4f
import org.joml.Vector2d
import kotlin.math.cos
import kotlin.math.sin
class AbilitySkillTreeComponent(
val ability: AbstractAbility<*>,
horizontalSizing: Sizing = Sizing.fill(50),
verticalSizing: Sizing = Sizing.fill(60),
val player: PlayerEntity = MinecraftClient.getInstance().player!!
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
val shadow = "textures/gui/shadow.png".toId()
val scrollChild = ScrollChild()
val scroll = ScrollContainerV2(Sizing.fill(), Sizing.fill(80), scrollChild)
var isHovered = false
init {
surface(Surface.PANEL)
alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP)
padding(Insets.of(5).withLeft(8).withRight(8))
//val scroll = Containers.horizontalScroll(Sizing.fill(25), Sizing.fill(60), scrollChild)
child(Containers.horizontalFlow(Sizing.fill(), Sizing.content(1)).apply {
child(ScalableLabelComponent(literalText {
text(ability.name.literal)
color = Colors.GRAY
}))
})
child(Containers.horizontalFlow(Sizing.fill(), Sizing.content(1)).apply {
child(ScalableLabelComponent(literalText {
text(ability.description)
color = Colors.GRAY
},0.75f).apply {
maxWidth(350)
})
})
child(scroll)
scroll.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
scroll.padding(Insets.of(2))
scrollChild.surface(Surface { context, component ->
val divider = 240f
RenderSystem.setShaderColor(divider / 255f, divider / 255f, divider / 255f, 1f);
context.drawTexture(
ability.getBackgroundTexture(),
component.x(),
component.y(),
0f,
0f,
component.width(),
component.height(),
16,
16
);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
})
scrollChild.alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP)
scrollChild.child(getComponent(SkillTreeUtils.toSkillTree(ability)))
child(XPComponent())
}
inner class XPComponent(
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content(),
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
init {
}
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
val textRenderer = MinecraftClient.getInstance().textRenderer
val string = "" + player.ffaPlayer.xp
val x = this.x() - textRenderer.getWidth(string) / 2
val y = this.y() + textRenderer.fontHeight / 2
context.drawText(textRenderer, string, x + 1, y, 0, false)
context.drawText(textRenderer, string, x - 1, y, 0, false)
context.drawText(textRenderer, string, x, y + 1, 0, false)
context.drawText(textRenderer, string, x, y - 1, 0, false)
context.drawText(textRenderer, string, x, y, 8453920, false)
}
}
inner class ScrollChild(
horizontalSizing: Sizing = Sizing.fixed(500),
verticalSizing: Sizing = Sizing.fixed(500),
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
override fun drawChildren(
context: OwoUIDrawContext,
mouseX: Int,
mouseY: Int,
partialTicks: Float,
delta: Float,
children: MutableList<out Component>
) {
super.drawChildren(context, mouseX, mouseY, partialTicks, delta, children)
}
}
private fun getComponent(node: TreeNode<ISkill>): Component {
val container = Wrapper(node, Sizing.content(), Sizing.content(), Algorithm.VERTICAL)
container.alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP)
// container.surface(Surface.outline(colors.random().argb()))
container.padding(Insets.of(1))
//container.debug()
if (node.children.isNotEmpty()) {
// Kinder horizontal anordnen
val childContainer = Containers.horizontalFlow(Sizing.content(), Sizing.content())
childContainer.id("child-node")
childContainer.alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP)
childContainer.padding(Insets.of(2)) // Fügt Abstand zwischen den Kindknoten hinzu
node.children.forEach {
childContainer.child(getComponent(it))
}
container.child(childContainer)
}
return container
}
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
RenderSystem.enableDepthTest()
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
val matrices = context.matrices
matrices.push()
matrices.translate(0f, 0f, 5.0f)
context.drawTexture(
shadow,
scroll.x(),
scroll.y(),
0f,
0f,
scroll.width(),
scroll.height(),
scroll.width(),
scroll.height(),
)
RenderSystem.disableBlend()
matrices.pop()
}
inner class Wrapper(
val node: TreeNode<ISkill>,
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content(),
algorithm: Algorithm
) : FlowLayout(horizontalSizing, verticalSizing, algorithm) {
val label = ScalableLabelComponent(node.value.title()).apply {
shadow(true)
}
var isHoveredChild = false
var isVisible = false
val box: FlowLayout = Containers.verticalFlow(Sizing.fixed(26), Sizing.fixed(26)).apply {
if (node.value.isUnlocked(MinecraftClient.getInstance().player!!)) {
} else {
}
//child(label)
cursorStyle(CursorStyle.POINTER)
mouseEnter().subscribe {
isHovered = true
isHoveredChild = true
}
mouseLeave().subscribe {
isHovered = false
isHoveredChild = false
}
mouseDown().subscribe { _, _, _ ->
if (isVisible) {
MinecraftClient.getInstance().soundManager.play(
PositionedSoundInstance.master(
SoundEvents.BLOCK_AMETHYST_BLOCK_HIT,
1.0f, 1f
)
)
node.value.skill()
}
return@subscribe true
}
child(node.value.icon())
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
}
init {
margins(Insets.top(5))
child(label)
child(box)
}
fun anchorPoint(): Component {
return box
}
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
if (isVisible) {
box.tooltip(node.value.tooltip(player))
} else {
box.tooltip(Text.empty())
}
val player = MinecraftClient.getInstance().player!!
var drawLines = true
if (node.value.isParentUnlocked(player)) {
if (node.value.isUnlocked(player)) {
if (node.value.parent() == null) {
box.surface { context2, component ->
val root = "textures/gui/root_panel.png".toId()
context2.drawTexture(
root,
component.x(),
component.y(),
0f,
0f,
26,
26,
26,
26
)
}
} else {
box.surface { owoUIDrawContext, parentComponent ->
if (node.value.isLast()) {
val root =
Identifier.ofVanilla("textures/gui/sprites/advancements/goal_frame_obtained.png")
owoUIDrawContext.drawTexture(
root,
parentComponent.x(),
parentComponent.y(),
0f,
0f,
26,
26,
26,
26
)
} else {
NinePatchTexture.draw("unlocked".toId(), context, parentComponent)
}
}
}
} else {
if (node.value.progress(player) == 1.0) {
box.surface { context2, component ->
val root = Identifier.ofVanilla("textures/gui/sprites/advancements/goal_frame_obtained.png")
context2.drawTexture(
root,
component.x(),
component.y(),
0f,
0f,
26,
26,
26,
26
)
}
} else {
if (node.value.isLast()) {
box.surface { owoUIDrawContext, parentComponent ->
val root =
Identifier.ofVanilla("textures/gui/sprites/advancements/goal_frame_unobtained.png")
owoUIDrawContext.drawTexture(
root,
parentComponent.x(),
parentComponent.y(),
0f,
0f,
26,
26,
26,
26
)
}
} else {
box.surface(Surface.PANEL)
}
}
}
} else {
if (!node.value.isUnlocked(player) && node.value.parent()?.isParentUnlocked(player) == true) {
if (node.value.isLast()) {
box.surface { owoUIDrawContext, parentComponent ->
val root = "textures/gui/goal_frame_dark.png".toId()
owoUIDrawContext.drawTexture(
root,
parentComponent.x(),
parentComponent.y(),
0f,
0f,
26,
26,
26,
26
)
}
} else {
box.surface(Surface.DARK_PANEL)
}
drawLines = false
} else {
isVisible = false
return
}
}
isVisible = true
if (!node.value.isUnlocked(MinecraftClient.getInstance().player!!)) {
// return
}
val childContainer = childById(FlowLayout::class.java, "child-node") as? FlowLayout? ?: return super.draw(
context,
mouseX,
mouseY,
partialTicks,
delta
)
if (childContainer.children().isEmpty()) return super.draw(context, mouseX, mouseY, partialTicks, delta)
val parentX = anchorPoint().x() + anchorPoint().width() / 2
val parentY = anchorPoint().y() + anchorPoint().height() - 5
// Berechne die mittlere Y-Position für die horizontale Linie
val lowestChildY =
childContainer.children().minOfOrNull { (it as? Wrapper)?.anchorPoint()?.y() ?: Int.MAX_VALUE }
?: return super.draw(context, mouseX, mouseX, partialTicks, delta)
val midY = parentY + (lowestChildY - parentY) / 2
val whiteLineThickness = 2.0
val grayedColor = Color.ofRgb(Colors.GRAY)
val progressColor = Color.GREEN// Farbe für die Fortschrittslinie
// Zeichne die graue Linie und die grüne Linie
for ((index, child) in childContainer.children().withIndex()) {
if (child is Wrapper) {
val childX = child.anchorPoint().x() + (child.anchorPoint().width()) / 2
val childY = child.anchorPoint().y()
// Zeichne die graue Linie
val angle =
Math.toDegrees(Math.atan2((childY - parentY).toDouble(), (childX - parentX).toDouble()));
val length =
Math.sqrt(((childX - parentX) * (childX - parentX) + (childY - parentY) * (childY - parentY)).toDouble());
if (drawLines) {
context.drawLine(parentX, parentY, angle, length, whiteLineThickness * 2, Color.BLACK);
context.drawLine(parentX, parentY, angle, length, whiteLineThickness, grayedColor);
}
val progress = child.node.value.progress(player)
// Berechne den Fortschritt-Endpunkt
val progressEndX = parentX + (childX - parentX) * progress
val progressEndY =
parentY + (childY - parentY) * progress / ((childY - parentY).toDouble().takeIf { it != 0.0 }
?: 1.0)
// Zeichne die grüne Linie über der grauen Linie
val progressAngle = angle; // Verwende denselben Winkel
val progressLength = length * progress; // Berechne die Länge basierend auf dem Fortschritt
if (drawLines) {
context.drawLine(
parentX,
parentY,
progressAngle,
progressLength,
whiteLineThickness,
progressColor
);
}
}
}
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
fun OwoUIDrawContext.drawLine(x1: Int, y1: Int, angle: Double, length: Double, thickness: Double, color: Color) {
// Berechne die Endpunkte der Linie basierend auf dem Winkel
val radians = Math.toRadians(angle)
val x2 = (x1 + cos(radians) * length).toInt()
val y2 = (y1 + sin(radians) * length).toInt()
val offset =
(Vector2d((x2 - x1).toDouble(), (y2 - y1).toDouble())).perpendicular().normalize().mul(thickness * 0.5f)
val buffer = Tessellator.getInstance().begin(DrawMode.QUADS, VertexFormats.POSITION_COLOR)
val matrix: Matrix4f = this.getMatrices().peek().getPositionMatrix()
val vColor = color.argb()
buffer.vertex(matrix, (x1.toDouble() + offset.x).toFloat(), (y1.toDouble() + offset.y).toFloat(), 0.0f)
.color(vColor)
buffer.vertex(matrix, (x1.toDouble() - offset.x).toFloat(), (y1.toDouble() - offset.y).toFloat(), 0.0f)
.color(vColor)
buffer.vertex(matrix, (x2.toDouble() - offset.x).toFloat(), (y2.toDouble() - offset.y).toFloat(), 0.0f)
.color(vColor)
buffer.vertex(matrix, (x2.toDouble() + offset.x).toFloat(), (y2.toDouble() + offset.y).toFloat(), 0.0f)
.color(vColor)
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.setShader { GameRenderer.getPositionColorProgram() }
BufferRenderer.drawWithGlobalProgram(buffer.end())
}
}
@@ -0,0 +1,123 @@
package gg.norisk.heroes.client.ui.skilltree
//import me.cortex.nvidium.Nvidium
import gg.norisk.heroes.client.ui.components.HeroListComponentV2
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.ui.components.LabelButtonComponent
import gg.norisk.ui.components.ScalableLabelComponent
import io.wispforest.owo.ui.base.BaseOwoScreen
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.*
import io.wispforest.owo.ui.util.UISounds
import me.cortex.nvidium.Nvidium
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.MinecraftClient
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
import java.awt.Color
class HeroSelectorScreenV2(val heroes: List<Hero>, val isKitEditorEnabled: Boolean = false) :
BaseOwoScreen<FlowLayout>() {
var hero: Hero? = null
set(value) {
heroInfoComponent?.remove()
centerLabel.remove()
if (field != value) {
field = value
if (value != null) {
heroInfoComponent = heroAbility(value)
this.uiAdapter.rootComponent.child(heroInfoComponent)
}
} else {
field = null
this.uiAdapter.rootComponent.child(centerLabel)
}
}
var heroInfoComponent: FlowLayout? = null
var centerLabel: FlowLayout = Containers.verticalFlow(Sizing.fill(), Sizing.content()).apply {
child(ScalableLabelComponent(literalText {
text("CHOOSE YOUR HERO")
}, 3f).apply {
shadow(true)
})
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
positioning(Positioning.relative(30, 40))
}
override fun createAdapter(): OwoUIAdapter<FlowLayout> {
return OwoUIAdapter.create(this, Containers::verticalFlow);
}
val adapter get() = uiAdapter.rootComponent
private fun heroAbility(hero: Hero): FlowLayout {
val container = Containers.verticalFlow(Sizing.content(), Sizing.content())
.apply { positioning(Positioning.relative(50, 30)) }
container.child(ScalableLabelComponent(literalText {
text(hero.name) {
}
}, 3f).apply {
shadow(true)
})
container.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
container.child(LabelButtonComponent("SKILL TREE".literal, Color.YELLOW).apply {
label.scale = 1.5f
mouseDown().subscribe { _, _, _ ->
UISounds.playButtonSound()
buildSkillTree(hero)
return@subscribe true
}
})
return container
}
private fun buildSkillTree(hero: Hero) {
uiAdapter.rootComponent.child(Containers.overlay(SkillTreeWrapper(hero)))
}
private class XpLabel(scale: Float = 1f) : ScalableLabelComponent("".literal, scale) {
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
text(literalText {
text("XP: ")
text((MinecraftClient.getInstance().player?.ffaPlayer?.xp ?: 0).toString())
})
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
override fun close() {
super.close()
if (FabricLoader.getInstance().isModLoaded("nvidium")) {
Nvidium.FORCE_DISABLE = false
this.client?.worldRenderer?.reload()
}
}
override fun build(root: FlowLayout) {
val heroList = HeroListComponentV2(heroes, this)
heroList.positioning(Positioning.relative(50, 90))
root.child(heroList)
if (hero == null) {
root.child(centerLabel)
}
if (FabricLoader.getInstance().isModLoaded("nvidium")) {
Nvidium.FORCE_DISABLE = true
this.client?.worldRenderer?.reload()
}
}
override fun shouldPause(): Boolean {
return false
}
override fun shouldCloseOnEsc(): Boolean {
return FabricLoader.getInstance().isDevelopmentEnvironment
}
}
@@ -0,0 +1,17 @@
package gg.norisk.heroes.client.ui.skilltree
import io.wispforest.owo.ui.core.Component
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
interface ISkill {
fun isUnlocked(player: PlayerEntity): Boolean
fun isParentUnlocked(player: PlayerEntity): Boolean
fun title(): Text
fun parent(): ISkill?
fun progress(player: PlayerEntity): Double
fun skill()
fun isLast(): Boolean
fun tooltip(player: PlayerEntity): Text
fun icon(): Component?
}
@@ -0,0 +1,30 @@
package gg.norisk.heroes.client.ui.skilltree
//import me.cortex.nvidium.Nvidium
import gg.norisk.heroes.common.hero.getHero
import io.wispforest.owo.ui.base.BaseOwoScreen
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.HorizontalAlignment
import io.wispforest.owo.ui.core.OwoUIAdapter
import io.wispforest.owo.ui.core.VerticalAlignment
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
class SkillTreeScreen : BaseOwoScreen<FlowLayout>() {
override fun createAdapter(): OwoUIAdapter<FlowLayout> {
return OwoUIAdapter.create(this, Containers::verticalFlow);
}
override fun build(root: FlowLayout) {
root.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
root.child(SkillTreeWrapper(MinecraftClient.getInstance().player?.getHero() ?: return))
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
}
}
@@ -0,0 +1,186 @@
package gg.norisk.heroes.client.ui.skilltree
import gg.norisk.heroes.common.ability.CooldownProperty
import gg.norisk.heroes.common.ability.SingleUseProperty
import gg.norisk.heroes.common.command.DebugCommand.getProgressBar
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.common.hero.ability.SkillPropertyPacket
import gg.norisk.heroes.common.networking.Networking
import io.wispforest.owo.ui.core.Component
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.silkmc.silk.core.text.literalText
object SkillTreeUtils {
fun toSkillTree(ability: AbstractAbility<*>): TreeNode<ISkill> {
val root = TreeNode<ISkill>(object : ISkill {
override fun isUnlocked(player: PlayerEntity): Boolean {
return true
}
override fun isParentUnlocked(player: PlayerEntity): Boolean {
return true
}
override fun title(): Text {
return Text.translatable(ability.name)
}
override fun parent(): ISkill? {
return null
}
override fun progress(player: PlayerEntity): Double {
return 1.0
}
override fun skill() {
}
override fun isLast(): Boolean {
return false
}
override fun tooltip(player: PlayerEntity): Text {
return Text.empty()
}
override fun icon(): Component {
return ability.getIconComponent()
}
})
for (property in ability.getAllProperties()) {
if (property is SingleUseProperty) continue
if (property is CooldownProperty) {
if (property.name == "NoCooldown") continue
}
var lastChild: TreeNode<ISkill>? = root
repeat(property.maxLevel) { level ->
val newChild = TreeNode<ISkill>(object : ISkill {
val parent = lastChild?.value
override fun isUnlocked(player: PlayerEntity): Boolean {
val levelInfo = property.getLevelInfo(player.uuid)
levelInfo.percentageTillNextLevel
return levelInfo.currentLevel > level
}
override fun isParentUnlocked(player: PlayerEntity): Boolean {
val levelInfo = property.getLevelInfo(player.uuid)
return levelInfo.currentLevel > level - 1
}
override fun title(): Text {
return literalText {
text(Text.translatable(property.translationKey))
text(" ")
text(intToRoman(level + 1))
}
}
override fun parent(): ISkill? {
return parent
}
override fun progress(player: PlayerEntity): Double {
val levelInfo = property.getLevelInfo(player.uuid)
if (levelInfo.currentLevel == level) {
return levelInfo.percentageTillNextLevel / 100.0
} else if (levelInfo.currentLevel > level) {
return 1.0
} else {
return 0.0
}
}
override fun skill() {
Networking.c2sSkillProperty.send(
SkillPropertyPacket(
ability.hero.internalKey, ability.internalKey, property.internalKey
)
)
}
override fun isLast(): Boolean {
return level >= property.maxLevel - 1
}
private fun <T> getValueText(value: T): Text {
return literalText {
// Überprüfe, ob der Wert eine Zahl ist
val formattedValue = when (value) {
is Number -> {
val doubleValue = value.toDouble()
// Überprüfe, ob der Wert Nachkommastellen hat
if (doubleValue % 1.0 == 0.0) {
doubleValue.toInt().toString() // Keine Nachkommastellen, nur die ganze Zahl
} else {
String.format("%.3f", doubleValue) // Formatiere auf 3 Nachkommastellen
}
}
else -> value.toString() // Andernfalls einfach den Wert als String
}
text(formattedValue)
if (property is CooldownProperty) {
text("s")
}
}
}
override fun tooltip(player: PlayerEntity): Text {
val levelInfo = property.getLevelInfo(player.uuid, level)
return literalText {
text("[")
text(Text.translatable(property.translationKey))
text("]")
newLine()
text(Text.translatable(property.descriptionKey))
emptyLine()
text("[")
text("Progress")
text("]")
newLine()
text(
getProgressBar(
levelInfo.percentageTillNextLevel,
100.0,
50,
"|".single()
)
)
text(" ${String.format("%.2f", levelInfo.percentageTillNextLevel)}%")
emptyLine()
text(getValueText(property.getValue(level + 1)))
}
}
override fun icon(): Component {
return property.icon.invoke()
}
})
lastChild?.addChild(newChild)
lastChild = newChild
}
}
return root
}
private val m_k = listOf(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
private val m_v = listOf("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I")
fun intToRoman(num: Int): String {
var str = ""
var n = num
for (i in m_k.indices) {
while (n >= m_k[i]) {
n -= m_k[i]
str += m_v[i]
}
}
return str
}
}
@@ -0,0 +1,186 @@
package gg.norisk.heroes.client.ui.skilltree
import gg.norisk.heroes.common.HeroesManager.toId
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import io.wispforest.owo.ui.component.Components
import io.wispforest.owo.ui.component.TextureComponent
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.*
import io.wispforest.owo.ui.util.UISounds
import net.minecraft.client.MinecraftClient
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.sound.SoundEvents
import net.minecraft.util.Colors
import net.minecraft.util.Identifier
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
class SkillTreeWrapper(
val hero: Hero,
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
val skillTreeWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content())
val tabs = mutableListOf<TabButton>()
init {
var index = 0
for ((name, ability) in hero.abilities) {
tabs += TabButton(ability, index)
index++
}
child(TabWrapper().apply {
children(tabs)
//zIndex(5000)
allowOverflow(true)
gap(2)
})
child(skillTreeWrapper)
tabs.first { it.ability.hasUnlocked(MinecraftClient.getInstance().player!!) }.apply {
this.isSelected = true
this.onClick()
}
//children(tabs)
}
override fun drawChildren(
context: OwoUIDrawContext,
mouseX: Int,
mouseY: Int,
partialTicks: Float,
delta: Float,
children: MutableList<out Component>
) {
super.drawChildren(context, mouseX, mouseY, partialTicks, delta, children.reversed())
}
override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
inner class TabWrapper(
horizontalSizing: Sizing = Sizing.content(),
verticalSizing: Sizing = Sizing.content()
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.HORIZONTAL) {
}
inner class TabButton(
val ability: AbstractAbility<*>,
index: Int,
horizontalSizing: Sizing = Sizing.fixed(28),
verticalSizing: Sizing = Sizing.fixed(28)
) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) {
var page = AbilitySkillTreeComponent(ability)
var isSelected = false
var item = ability.getIconComponent().id("unlocked")
var lockIcon = Components.texture("textures/gui/lock_icon.png".toId(), 0, 0, 20, 20, 20, 20).apply {
id("locked")
tooltip(literalText {
text(ability.name)
newLine()
text(ability.getUnlockCondition()) {
color = Colors.LIGHT_GRAY
}
})
}
init {
surface { context, container ->
context.matrices.push()
val texture = if (isSelected) {
if (index == 0) {
Identifier.of("textures/gui/sprites/advancements/tab_above_left_selected.png")
} else {
Identifier.of("textures/gui/sprites/advancements/tab_above_middle_selected.png")
}
} else {
if (index == 0) {
Identifier.of("textures/gui/sprites/advancements/tab_above_left.png")
} else {
Identifier.of("textures/gui/sprites/advancements/tab_above_middle.png")
}
}
context.drawTexture(
texture,
container.x(),
container.y(),
0f,
0f,
28,
32,
28,
32
)
// NinePatchTexture.draw(OwoUIDrawContext.PANEL_NINE_PATCH_TEXTURE, context, container.x(), container.y(), container.width(), container.height())
context.matrices.pop()
}
mouseDown().subscribe { _, _, _ ->
if (!ability.hasUnlocked(MinecraftClient.getInstance().player!!)) {
MinecraftClient.getInstance().soundManager.play(
PositionedSoundInstance.master(
SoundEvents.ENTITY_VILLAGER_NO,
1.0f
)
)
return@subscribe true
}
if (!isSelected) {
UISounds.playInteractionSound()
isSelected = !isSelected
tabs.filter { it != this }.forEach { it.isSelected = false }
onClick()
}
return@subscribe true
}
tooltip(ability.name.literal)
alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)
//positioning(Positioning.absolute(0, 0))
child(lockIcon)
//zIndex(-5000)
allowOverflow(true)
}
fun onClick() {
if (isSelected) {
skillTreeWrapper.clearChildren()
skillTreeWrapper.child(page)
}
}
override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) {
val hasUnlocked = ability.hasUnlocked(MinecraftClient.getInstance().player!!)
val locked = childById(TextureComponent::class.java, "locked")
val unlocked = childById(Component::class.java, "unlocked")
val component = if (hasUnlocked) {
if (unlocked == null) {
child(item)
}
if (locked != null) {
removeChild(locked)
}
item
} else {
if (locked == null) {
child(lockIcon)
}
if (unlocked != null) {
removeChild(item)
}
lockIcon
}
if (isSelected) {
component.margins(Insets.none())
} else {
component.margins(Insets.top(8))
}
super.draw(context, mouseX, mouseY, partialTicks, delta)
}
}
}
@@ -0,0 +1,28 @@
package gg.norisk.heroes.client.ui.skilltree
class TreeNode<T>(val value: T) {
val children: MutableList<TreeNode<T>> = mutableListOf()
fun addChild(child: TreeNode<T>): TreeNode<T> {
children.add(child)
return this
}
// Tiefensuche (Depth-First Search)
fun dfs(visit: (T) -> Unit) {
visit(value)
children.forEach { it.dfs(visit) }
}
// Breitensuche (Breadth-First Search)
fun bfs(visit: (T) -> Unit) {
val queue = ArrayDeque<TreeNode<T>>()
queue.add(this)
while (queue.isNotEmpty()) {
val currentNode = queue.removeFirst()
visit(currentNode.value)
currentNode.children.forEach { queue.add(it) }
}
}
}
@@ -0,0 +1,76 @@
package gg.norisk.heroes.common
import gg.norisk.heroes.common.registry.SoundRegistry
import gg.norisk.heroes.server.HeroesManagerServer
import net.fabricmc.api.EnvType
import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.resource.featuretoggle.FeatureFlag
import net.minecraft.util.Identifier
import net.minecraft.util.WorldSavePath
import net.silkmc.silk.core.text.literalText
import org.apache.logging.log4j.LogManager
import java.io.File
import java.nio.file.Path
object HeroesManager : ModInitializer {
const val MOD_ID = "hero-api"
var baseDirectory: File = getBasePath(null)
val logger = LogManager.getLogger(MOD_ID)
fun String.toId() = Identifier.of(MOD_ID, this)
lateinit var heroesFlag: FeatureFlag
val prefix
get() = literalText {
text("[") { }
text("Heroes") { }
text("]") { }
text(" ")
}
val isServer get() = FabricLoader.getInstance().isDevelopmentEnvironment || FabricLoader.getInstance().environmentType == EnvType.SERVER
val isClient get() = FabricLoader.getInstance().isDevelopmentEnvironment || FabricLoader.getInstance().environmentType == EnvType.CLIENT
override fun onInitialize() {
logger.info("Init Hero-Api Common...")
SoundRegistry.init()
HeroesManagerServer.initServer()
ServerLifecycleEvents.SERVER_STARTING.register {
setBasePath(it.getSavePath(WorldSavePath("heroes")))
logger.info("Found Server Path: ${baseDirectory}")
}
}
private fun getBasePath(serverPath: Path?): File {
val defaultPath = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) {
FabricLoader.getInstance().configDir
} else {
serverPath ?: FabricLoader.getInstance().configDir
}
val baseDirectory = File(
System.getProperty(
"hero_folder_path",
defaultPath.toFile().absolutePath
),
).apply {
mkdirs()
}
return baseDirectory
}
private fun setBasePath(serverPath: Path?) {
this.baseDirectory = getBasePath(serverPath)
}
fun client(callBack: () -> Unit) {
if (FabricLoader.getInstance().environmentType == EnvType.CLIENT) {
callBack.invoke()
}
}
}
@@ -0,0 +1,50 @@
package gg.norisk.heroes.common.ability
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.ability.operation.AddValueTotal
import gg.norisk.heroes.common.ability.operation.MultiplyBase
import gg.norisk.heroes.common.ability.operation.Operation
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement
import java.util.*
@Serializable
sealed class AbstractNumberProperty : PlayerProperty<Double>() {
abstract var modifier: Operation
override fun getValue(uuid: UUID): Double {
return getValue(getLevelInfo(uuid).currentLevel)
}
override fun getValue(int: Int): Double {
return when (modifier) {
is AddValueTotal, is MultiplyBase -> {
modifier.getOperatedValue(baseValue, int)
}
}
}
override fun fromJson(text: String) {
runCatching {
val loaded = JSON.decodeFromString<NumberProperty>(text)
baseValue = loaded.baseValue
maxLevel = loaded.maxLevel
name = loaded.name
levelScale = loaded.levelScale
modifier = loaded.modifier
}.onFailure {
HeroesManager.logger.error("Error Loading $name ${it.message}")
it.printStackTrace()
}
}
override fun toJson(): String {
return JSON.encodeToString<PlayerProperty<Double>>(this)
}
override fun toJsonElement(): JsonElement {
return JSON.encodeToJsonElement<PlayerProperty<Double>>(this)
}
}
@@ -0,0 +1,6 @@
package gg.norisk.heroes.common.ability
import kotlinx.serialization.Serializable
@Serializable
sealed class AbstractUsageProperty : AbstractNumberProperty()
@@ -0,0 +1,15 @@
package gg.norisk.heroes.common.ability
import gg.norisk.heroes.common.ability.operation.Operation
import kotlinx.serialization.Serializable
@Serializable
open class CooldownProperty(
override var baseValue: Double,
override var maxLevel: Int,
override var name: String,
override var modifier: Operation,
override var levelScale: Int = PlayerProperty.levelScale
) : AbstractNumberProperty() {
}
@@ -0,0 +1,12 @@
package gg.norisk.heroes.common.ability
data class LevelInformation(
val currentLevel: Int,
val nextLevel: Int,
val xpCurrentLevel: Int,
val xpNextLevel: Int,
val xpTillNextLevel: Int,
val percentageTillNextLevel: Double,
val experiencePoints: Int,
val maxLevel: Int,
)
@@ -0,0 +1,18 @@
package gg.norisk.heroes.common.ability
import gg.norisk.heroes.common.ability.operation.Operation
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.*
@Serializable
class MultiUseProperty(
override var baseValue: Double,
override var maxLevel: Int,
override var name: String,
override var modifier: Operation,
override var levelScale: Int = PlayerProperty.levelScale
) : AbstractUsageProperty() {
@Transient
val uses = mutableMapOf<UUID, Int>()
}
@@ -0,0 +1,16 @@
package gg.norisk.heroes.common.ability
import gg.norisk.heroes.common.ability.operation.Operation
import kotlinx.serialization.Serializable
@Serializable
class NumberProperty(
override var baseValue: Double,
override var maxLevel: Int,
override var name: String,
override var modifier: Operation,
override var levelScale: Int = PlayerProperty.levelScale,
) : AbstractNumberProperty() {
}
@@ -0,0 +1,149 @@
package gg.norisk.heroes.common.ability
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.server.database.player.PlayerProvider
import io.wispforest.owo.ui.component.Components
import io.wispforest.owo.ui.core.Component
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import net.minecraft.item.Items
import java.util.*
import kotlin.math.cbrt
import kotlin.math.pow
@Serializable
sealed class PlayerProperty<T> {
abstract var baseValue: T
abstract var maxLevel: Int
abstract var name: String
abstract var levelScale: Int
@Transient
var icon: () -> Component = {
Components.item(Items.CLOCK.defaultStack)
}
@Transient
lateinit var hero: Hero
@Transient
lateinit var ability: AbstractAbility<*>
companion object {
val levelScale = 250
val JSON = Json {
prettyPrint = true
encodeDefaults = true
ignoreUnknownKeys = true
//explicitNulls = true
}
}
abstract fun getValue(uuid: UUID): T
abstract fun getValue(int: Int): T
abstract fun fromJson(text: String)
abstract fun toJson(): String
abstract fun toJsonElement(): JsonElement
open fun getMaxValue(): T {
return getValue(maxLevel)
}
fun addExperience(uuid: UUID, experienceToAdd: Int): Int {
val player = getOrLoadPlayer(uuid)
val maxExperience = getXpForLevel(maxLevel)
val buffer = player.experiencePoints + experienceToAdd
return if (buffer > maxExperience) {
val toAdd = maxExperience - player.experiencePoints // Nur bis zum Maximalwert hinzufügen
player.experiencePoints = maxExperience // Korrigiere Erfahrungspunkte auf Maximum
toAdd
} else {
player.experiencePoints += experienceToAdd
experienceToAdd
}
}
fun isMaxed(uuid: UUID): Boolean {
return getLevelInfo(uuid).currentLevel >= maxLevel
}
fun getLevelInfo(uuid: UUID, level: Int? = null): LevelInformation {
val player = getOrLoadPlayer(uuid)
val currentLevel = Math.min(maxLevel, level ?: calculateLevel(player.experiencePoints))
val nextLevel = Math.min(maxLevel, currentLevel + 1)
val xpCurrentLevel = getXpForLevel(currentLevel)
val xpNextLevel = if (currentLevel < maxLevel) {
getXpForLevel(nextLevel)
} else {
xpCurrentLevel // Kein weiteres Level, bleibt gleich
}
val xpTillNextLevel = if (currentLevel < maxLevel) {
xpNextLevel - player.experiencePoints
} else {
0 // Kein weiteres Level
}
val percentageTillNextLevel = if (currentLevel < maxLevel) {
Math.max(
0.0, Math.min(
100.0,
((player.experiencePoints - xpCurrentLevel).toDouble() / (xpNextLevel - xpCurrentLevel).toDouble()) * 100.0
)
)
} else {
100.0 // Max-Level erreicht
}
if (currentLevel == maxLevel - 1 && percentageTillNextLevel >= 100f) {
return LevelInformation(
maxLevel,
maxLevel,
xpCurrentLevel,
xpNextLevel,
xpTillNextLevel,
percentageTillNextLevel,
player.experiencePoints,
maxLevel
)
}
return LevelInformation(
currentLevel,
nextLevel,
xpCurrentLevel,
xpNextLevel,
xpTillNextLevel,
percentageTillNextLevel,
player.experiencePoints,
maxLevel
)
}
private fun calculateLevel(xp: Int): Int {
return cbrt((xp / levelScale).toDouble()).toInt()
}
private fun getXpForLevel(level: Int): Int {
return (levelScale * level.toDouble().pow(3)).toInt()
}
val internalKey get() = name.lowercase().replace(" ", "_")
val translationKey get() = "heroes.property.${internalKey}"
val descriptionKey get() = "heroes.property.${internalKey}.description"
private fun getOrLoadPlayer(uuid: UUID): PropertyPlayer {
val player = runBlocking { PlayerProvider.get(uuid) }
val heroMap = player.heroes.computeIfAbsent(hero.internalKey) { mutableMapOf() }
val abilityMap = heroMap.computeIfAbsent(ability.internalKey) { mutableMapOf() }
val property = abilityMap.computeIfAbsent(internalKey) { PropertyPlayer() }
return property
}
}
@@ -0,0 +1,9 @@
package gg.norisk.heroes.common.ability
import kotlinx.serialization.Serializable
@Serializable
data class PropertyPlayer(
var experiencePoints: Int = 0
) {
}
@@ -0,0 +1,15 @@
package gg.norisk.heroes.common.ability
import gg.norisk.heroes.common.ability.operation.Operation
import kotlinx.serialization.Serializable
@Serializable
class SingleUseProperty(
override var baseValue: Double,
override var maxLevel: Int,
override var name: String,
override var modifier: Operation,
override var levelScale: Int = PlayerProperty.levelScale
) : AbstractUsageProperty() {
}
@@ -0,0 +1,20 @@
package gg.norisk.heroes.common.ability.operation
import kotlinx.serialization.Serializable
@Serializable
class AddValueTotal(var steps: List<Double>) : Operation() {
constructor(vararg steps: Double) : this(steps.toList())
override fun getOperatedValue(baseValue: Double, level: Int): Double {
// wir doublen alles wegen... jo
var valueToReturn = baseValue
repeat(level) {
val increment = steps.getOrNull(it) ?: error("$steps doesn't have an index for level $it")
valueToReturn += increment
}
return valueToReturn
}
}
@@ -0,0 +1,14 @@
package gg.norisk.heroes.common.ability.operation
import kotlinx.serialization.Serializable
@Serializable
class MultiplyBase(var steps: List<Double>) : Operation() {
constructor(vararg steps: Double) : this(steps.toList())
override fun getOperatedValue(baseValue: Double, level: Int): Double {
//wir doublen alles wegen... jo
val increment = steps.getOrNull(level) ?: error("$steps doesn't have an index for level $level")
return baseValue * increment
}
}
@@ -0,0 +1,8 @@
package gg.norisk.heroes.common.ability.operation
import kotlinx.serialization.Serializable
@Serializable
sealed class Operation {
abstract fun getOperatedValue(baseValue: Double, level: Int): Double
}
@@ -0,0 +1,224 @@
package gg.norisk.heroes.common.command
import com.mojang.brigadier.context.CommandContext
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.ability.PlayerProperty
import gg.norisk.heroes.common.command.EditPropertyCommand.editCommand
import gg.norisk.heroes.common.ffa.KitEditorManager
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.hero.HeroManager
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.heroes.server.database.player.PlayerProvider
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.command.argument.EntityArgumentType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.silkmc.silk.commands.PermissionLevel
import net.silkmc.silk.commands.command
import net.silkmc.silk.core.text.literal
import net.silkmc.silk.core.text.literalText
object DebugCommand {
fun initServer() {
command("heroes") {
requiresPermissionLevel(PermissionLevel.OWNER)
//if (HeroesManager.isClient) {
requires { it.server.saveProperties.dataConfiguration.enabledFeatures.contains(HeroesManager.heroesFlag) }
// }
runs {
Networking.s2cHeroSelectorPacket.send(
HeroSelectorPacket(HeroManager.registeredHeroes.keys.toList(), true, KitEditorManager.hasKitWorld),
this.source.playerOrThrow
)
}
literal("reload") {
runs {
HeroManager.reloadHeroes(*HeroManager.registeredHeroes.values.toTypedArray())
}
}
literal("xp") {
literal("set") {
argument("players", EntityArgumentType.players()) {
argument<Int>("xp") { xp ->
runsAsync {
val players = EntityArgumentType.getPlayers(this, "players")
for (player in players) {
val cachedPlayer = PlayerProvider.get(player.uuid)
cachedPlayer.xp = xp()
player.ffaPlayer = cachedPlayer
PlayerProvider.save(player.ffaPlayer)
this.source.sendMessage("Set Xp of ${player.gameProfile.name} to ${xp()}".literal)
}
}
}
}
}
}
argument<String>("hero") { heroKey ->
suggestList { HeroManager.registeredHeroes.keys }
literal("ability") {
argument<String>("ability") { abilityKey ->
suggestList {
HeroManager.getHero(heroKey(it))?.abilities?.keys
}
literal("property") {
argument<String>("property") { propertyKey ->
suggestList {
HeroManager.getHero(heroKey(it))?.abilities
?.values
?.map { ability -> ability.getAllProperties() }
?.flatten()?.map { property -> property.internalKey }
}
editCommand()
literal("add") {
argument<Int>("expierencepoints") { xpPoints ->
runs {
val hero = HeroManager.getHero(heroKey())!!
val ability = hero.abilities.values
.flatMap { ability ->
ability.getAllProperties().map { property -> ability to property }
}
.firstOrNull { (ability, property) ->
property.internalKey == propertyKey() && ability.internalKey == abilityKey()
}!!
val player = this.source.playerOrThrow
ability.second.addExperience(player.uuid, xpPoints())
sendLevelInfo(player, player, ability.second, ability.first)
}
}
}
literal("info") {
runs {
val hero = HeroManager.getHero(heroKey())!!
val ability = hero.abilities.values
.flatMap { ability ->
ability.getAllProperties().map { property -> ability to property }
}
.firstOrNull { (ability, property) ->
property.internalKey == propertyKey() && ability.internalKey == abilityKey()
}!!
val player = this.source.playerOrThrow
sendLevelInfo(player, player, ability.second, ability.first)
}
}
}
}
}
}
}
}
}
fun <S> CommandContext<S>.getHeroInformation(): Triple<Hero, AbstractAbility<*>, PlayerProperty<*>> {
val hero = HeroManager.getHero(this.getArgument("hero", String::class.java))!!
val propertyKey = this.getArgument("property", String::class.java)
val abilityKey = this.getArgument("ability", String::class.java)
val ability = hero.abilities.values
.flatMap { ability ->
ability.getAllProperties().map { property -> ability to property }
}
.firstOrNull { (ability, property) ->
property.internalKey == propertyKey && ability.internalKey == abilityKey
}!!
return Triple(hero, ability.first, ability.second)
}
fun PlayerProperty<*>.toDebugText(): Text {
return literalText {
text(Text.translatable(this@toDebugText.translationKey))
newLine()
text("Max Level: ${this@toDebugText.maxLevel}")
newLine()
text("Base Value: ${this@toDebugText.baseValue}")
}
}
fun PlayerEntity.sendDebugMessage(message: Text) {
if (FabricLoader.getInstance().isDevelopmentEnvironment) {
sendMessage(message)
}
}
fun sendLevelInfo(
player: PlayerEntity,
about: PlayerEntity,
property: PlayerProperty<*>,
ability: AbstractAbility<*>
) {
val levelInfo = property.getLevelInfo(about.uuid)
player.sendMessage(literalText {
emptyLine()
text("Level Info for ${property.name}") {
underline = true
}
emptyLine()
text("Player: ")
text(player.name)
text(" ${player.uuid}")
newLine()
text("Ability: ${ability.name}")
newLine()
text("Current Value: ${property.getValue(about.uuid)}")
newLine()
text("Current Level: ${levelInfo.currentLevel}/${levelInfo.maxLevel}")
newLine()
text("Next Level: ${levelInfo.nextLevel}")
newLine()
text("Step: ${levelInfo.experiencePoints}/${levelInfo.xpNextLevel}")
newLine()
text("Xp Needed for Upgrade: ${levelInfo.xpTillNextLevel}") { }
newLine()
text("Progress: ")
text(
getProgressBar(
levelInfo.percentageTillNextLevel,
100.0,
50,
"|".single()
)
)
text(" ${String.format("%.3f", levelInfo.percentageTillNextLevel)}%")
})
}
fun getProgressBar(
current: Double,
max: Double,
totalBars: Int,
symbol: Char,
completedColor: Int = getProgressBarColor(current, max),
notCompletedColor: Int = 0xa1a1a1
): Text {
val percent = current.toFloat() / max
val progressBars = (totalBars * percent).toInt()
return literalText {
repeat(progressBars) {
text("" + symbol) {
color = completedColor
}
}
repeat(totalBars - progressBars) {
text("" + symbol) {
color = notCompletedColor
}
}
}
}
fun getProgressBarColor(progress: Double, maxProgress: Double): Int {
val percentage = progress / maxProgress * 100.0
return when {
percentage > 66 -> 0x6fff36
percentage > 30 -> 0xfff700
else -> 0xff0000
}
}
}
@@ -0,0 +1,278 @@
package gg.norisk.heroes.common.command
import com.mojang.brigadier.arguments.StringArgumentType
import gg.norisk.heroes.common.ability.AbstractNumberProperty
import gg.norisk.heroes.common.ability.PlayerProperty
import gg.norisk.heroes.common.ability.operation.AddValueTotal
import gg.norisk.heroes.common.ability.operation.MultiplyBase
import gg.norisk.heroes.common.command.DebugCommand.getHeroInformation
import gg.norisk.heroes.server.config.ConfigManagerServer
import net.minecraft.server.command.ServerCommandSource
import net.silkmc.silk.commands.ArgumentCommandBuilder
import net.silkmc.silk.commands.LiteralCommandBuilder
import net.silkmc.silk.core.text.broadcastText
import java.awt.Color
object EditPropertyCommand {
fun ArgumentCommandBuilder<ServerCommandSource, String>.editCommand() {
literal("edit") {
baseValue()
maxLevel()
levelScale()
operation()
}
}
private fun LiteralCommandBuilder<ServerCommandSource>.operation() {
literal("operation") {
literal("type") {
argument<String>("type") { typeString ->
suggestList {
val type = (it.getHeroInformation().third as AbstractNumberProperty).modifier::class.simpleName
val set = mutableSetOf(type, MultiplyBase::class.simpleName, AddValueTotal::class.simpleName)
set.toList()
}
runs {
val (hero, ability, property) = this.getHeroInformation()
val oldModifier = (property as? AbstractNumberProperty?)?.modifier
val source = this.source.displayName
val newValue = when (oldModifier) {
is AddValueTotal -> {
oldModifier.steps
}
is MultiplyBase -> {
oldModifier.steps
}
null -> TODO()
}
when(typeString()) {
AddValueTotal::class.simpleName -> {
(property as? AbstractNumberProperty?)?.modifier = AddValueTotal(newValue)
}
MultiplyBase::class.simpleName -> {
(property as? AbstractNumberProperty?)?.modifier = MultiplyBase(newValue)
}
}
runCatching {
hero.save()
hero.load()
}.onSuccess {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier")
newLine()
text("- ${oldModifier::class.simpleName}") {
color = Color.RED.rgb
}
newLine()
text("+ ${(property as? AbstractNumberProperty?)!!.modifier::class.simpleName}") {
color = Color.GREEN.rgb
}
}
}.onFailure {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier")
newLine()
text("ERROR ${it.message}")
}
it.printStackTrace()
}.also {
ConfigManagerServer.sendHeroSettings(hero)
}
}
}
}
literal("list") {
argument<String>("values", StringArgumentType.string()) { valuesString ->
suggestList {
when (val modifier = (it.getHeroInformation().third as? AbstractNumberProperty?)?.modifier) {
is AddValueTotal -> listOf("\"${modifier.steps}\"")
is MultiplyBase -> listOf("\"${modifier.steps}\"")
else -> listOf("\"SCHREIB NORISK AN\"")
}
}
runs {
val (hero, ability, property) = this.getHeroInformation()
val modifier = (property as? AbstractNumberProperty?)?.modifier
val possibleList = valuesString()
var oldValue: Any? = null
val source = this.source.displayName
when (modifier) {
is AddValueTotal -> {
oldValue = modifier.steps
modifier.steps = PlayerProperty.JSON.decodeFromString<List<Double>>(possibleList)
}
is MultiplyBase -> {
oldValue = modifier.steps
modifier.steps = PlayerProperty.JSON.decodeFromString<List<Double>>(possibleList)
}
null -> TODO()
}
runCatching {
hero.save()
hero.load()
}.onSuccess {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier")
newLine()
val newValue = when (modifier) {
is AddValueTotal -> {
modifier.steps
}
is MultiplyBase -> {
modifier.steps
}
}
text("- $oldValue") {
color = Color.RED.rgb
}
newLine()
text("+ $newValue") {
color = Color.GREEN.rgb
}
}
}.onFailure {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier")
newLine()
text("ERROR ${it.message}")
}
it.printStackTrace()
}.also {
ConfigManagerServer.sendHeroSettings(hero)
}
}
}
}
}
}
private fun LiteralCommandBuilder<ServerCommandSource>.levelScale() {
literal("levelScale") {
argument<Int>("value") { value ->
suggestList {
listOf(it.getHeroInformation().third.levelScale)
}
runs {
val (hero, ability, property) = this.getHeroInformation()
if (property is AbstractNumberProperty) {
val oldValue = property.levelScale
property.levelScale = value()
val source = this.source.displayName
runCatching {
hero.save()
hero.load()
}.onSuccess {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, levelScale")
newLine()
text("from $oldValue to ${property.baseValue}")
}
}.onFailure {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, levelScale")
newLine()
text("ERROR ${it.message}")
}
it.printStackTrace()
}.also {
ConfigManagerServer.sendHeroSettings(hero)
}
}
}
}
}
}
private fun LiteralCommandBuilder<ServerCommandSource>.maxLevel() {
literal("maxLevel") {
argument<Int>("value") { value ->
suggestList {
listOf(it.getHeroInformation().third.maxLevel)
}
runs {
val (hero, ability, property) = this.getHeroInformation()
if (property is AbstractNumberProperty) {
val oldValue = property.maxLevel
property.maxLevel = value()
val source = this.source.displayName
runCatching {
hero.save()
hero.load()
}.onSuccess {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, maxLevel")
newLine()
text("from $oldValue to ${property.baseValue}")
}
}.onFailure {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, maxLevel")
newLine()
text("ERROR ${it.message}")
}
it.printStackTrace()
}.also {
ConfigManagerServer.sendHeroSettings(hero)
}
}
}
}
}
}
private fun LiteralCommandBuilder<ServerCommandSource>.baseValue() {
literal("baseValue") {
argument<String>("value") { value ->
suggestList {
listOf(it.getHeroInformation().third.baseValue)
}
runs {
val (hero, ability, property) = this.getHeroInformation()
if (property is AbstractNumberProperty) {
val oldValue = property.baseValue
property.baseValue = value().toDouble()
val source = this.source.displayName
runCatching {
hero.save()
hero.load()
}.onSuccess {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, baseValue")
newLine()
text("from $oldValue to ${property.baseValue}")
}
}.onFailure {
this.source.server.broadcastText {
text(source)
text(" changed ${hero.name}, ${ability.name}, ${property.name}, baseValue")
newLine()
text("ERROR ${it.message}")
}
it.printStackTrace()
}.also {
ConfigManagerServer.sendHeroSettings(hero)
}
}
}
}
}
}
}
@@ -0,0 +1,25 @@
package gg.norisk.heroes.common.cooldown
import kotlinx.serialization.Serializable
@Serializable
data class CooldownInfo(
val entityId: Int,
val duration: Long,
val startTime: Long?,
val currentTime: Long,
val multipleUsesInfo: MultipleUsesInfo?,
val heroKey: String,
val abilityKey: String,
val endTime: Long?,
var durationString: String? = null,
) {
val hasEnded get() = endTime?.let { System.nanoTime() > it } ?: true
val remaining get() = endTime?.let { it - System.nanoTime() } ?: 0
}
@Serializable
data class MultipleUsesInfo(
val currentUse: Int,
val maxUses: Int
)
@@ -0,0 +1,26 @@
package gg.norisk.heroes.common.database
import net.fabricmc.api.EnvType
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.server.network.ServerPlayerEntity
abstract class AbstractProvider<K, V> {
protected val cache = hashMapOf<K, V>()
abstract suspend fun save(data: V)
abstract suspend fun get(uuid: K): V
abstract suspend fun onPlayerJoin(player: ServerPlayerEntity)
abstract suspend fun onPlayerLeave(player: ServerPlayerEntity)
abstract fun getCachedClient(uuid: K): V?
protected suspend fun getCached(uuid: K): V? {
if (FabricLoader.getInstance().environmentType == EnvType.CLIENT && !FabricLoader.getInstance().isDevelopmentEnvironment) {
val cachedOnClient = getCachedClient(uuid)
if (cachedOnClient != null) {
return cachedOnClient
}
}
return cache[uuid]
}
}
@@ -0,0 +1,20 @@
package gg.norisk.heroes.common.database.inventory
import gg.norisk.heroes.common.database.AbstractProvider
import gg.norisk.heroes.common.player.InventorySorting
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.heroes.common.utils.PlayStyle
import net.minecraft.client.MinecraftClient
import net.minecraft.server.network.ServerPlayerEntity
import java.util.*
abstract class AbstractInventoryProvider(val playStyle: PlayStyle) : AbstractProvider<UUID, InventorySorting?>() {
override suspend fun onPlayerJoin(player: ServerPlayerEntity) {}
override suspend fun onPlayerLeave(player: ServerPlayerEntity) {}
override fun getCachedClient(uuid: UUID): InventorySorting? {
val ffaPlayer = MinecraftClient.getInstance().world?.getPlayerByUuid(uuid)?.ffaPlayer
return ffaPlayer?.inventorySorting
}
}
@@ -0,0 +1,65 @@
package gg.norisk.heroes.common.database.inventory
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.player.InventorySorting
import gg.norisk.heroes.common.utils.PlayStyle
import gg.norisk.heroes.common.utils.createIfNotExists
import gg.norisk.heroes.server.config.ConfigManagerServer.JSON
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import java.util.*
class JsonInventoryProvider : AbstractInventoryProvider(PlayStyle.current) {
private val file get() = HeroesManager.baseDirectory.resolve("player-inventory-database.json").createIfNotExists()
private fun loadDatabase(): MutableSet<InventorySorting> {
var database = mutableSetOf<InventorySorting>()
runCatching {
if (file.exists()) {
database = JSON.decodeFromString(file.readText())
}
}.onFailure {
if (file.readText().isBlank()) {
file.writeText("[]")
}
logger.info("Error Reading File ${file.absolutePath}")
it.printStackTrace()
}
return database
}
private suspend fun find(uuid: UUID): InventorySorting? {
val database = loadDatabase()
return database.find { it.userId == uuid && it.playStyle == playStyle }
}
override suspend fun get(uuid: UUID): InventorySorting? {
val inventory = getCached(uuid) ?: find(uuid)
cache[uuid] = inventory
return inventory
}
override suspend fun save(data: InventorySorting?) {
if (data == null) {
logger.info("Cant save Inventory `null`")
return
}
if (data.main.isEmpty() && data.armor.isEmpty() && data.offhand.isEmpty()) {
logger.info("${data.userId}'s inventory is empty. not saving")
return
}
val database = loadDatabase()
database.removeIf { it.userId == data.userId }
database.add(data)
if (!file.exists()) {
withContext(Dispatchers.IO) {
file.createNewFile()
}
}
file.writeText(JSON.encodeToString(database))
logger.info("Saved InventorySorting for ${data.userId} to $file")
}
}
@@ -0,0 +1,39 @@
package gg.norisk.heroes.common.database.player
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.database.AbstractProvider
import gg.norisk.heroes.common.player.FFAPlayer
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.heroes.server.database.inventory.InventoryProvider
import net.minecraft.client.MinecraftClient
import net.minecraft.server.network.ServerPlayerEntity
import java.util.UUID
abstract class AbstractPlayerProvider : AbstractProvider<UUID, FFAPlayer>() {
override fun getCachedClient(uuid: UUID): FFAPlayer? {
val ffaPlayer = MinecraftClient.getInstance().world?.getPlayerByUuid(uuid)?.ffaPlayer
return ffaPlayer
}
override suspend fun onPlayerJoin(player: ServerPlayerEntity) {
val ffaPlayer = get(player.uuid)
cache[player.uuid] = ffaPlayer
player.ffaPlayer = ffaPlayer
logger.info("Loaded Database Player ${player.gameProfile.name}")
InventoryProvider.onPlayerJoin(player)
}
override suspend fun onPlayerLeave(player: ServerPlayerEntity) {
if (cache.containsKey(player.uuid)) {
save(cache.computeIfAbsent(player.uuid) { FFAPlayer(player.uuid) })
cache.remove(player.uuid)
logger.info("Saving Database Player ${player.gameProfile.name}")
} else {
logger.warn("Cache didn't contain any data about ${player.gameProfile.name} (${player.gameProfile.id}), not saving any data")
}
InventoryProvider.onPlayerLeave(player)
}
}
@@ -0,0 +1,54 @@
package gg.norisk.heroes.common.database.player
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.player.FFAPlayer
import gg.norisk.heroes.server.config.ConfigManagerServer.JSON
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import java.util.*
class JsonPlayerProvider : AbstractPlayerProvider() {
private val file get() = HeroesManager.baseDirectory.resolve("player-database.json")
private suspend fun loadDatabase(): MutableSet<FFAPlayer> {
var database = mutableSetOf<FFAPlayer>()
runCatching {
if (file.exists()) {
database = JSON.decodeFromString(file.readText())
}
}.onFailure {
if (file.readText().isBlank()) {
file.writeText("[]")
}
logger.error("Error Reading ${file.absolutePath}")
it.printStackTrace()
}
return database
}
private suspend fun findPlayer(uuid: UUID): FFAPlayer? {
val database = loadDatabase()
return database.find { it.uuid == uuid }
}
override suspend fun get(uuid: UUID): FFAPlayer {
val player = getCached(uuid) ?: findPlayer(uuid) ?: FFAPlayer(uuid)
cache[uuid] = player
return player
}
override suspend fun save(data: FFAPlayer) {
val database = loadDatabase()
database.removeIf { it.uuid == data.uuid }
database.add(data.copy(inventorySorting = null))
if (!file.exists()) {
withContext(Dispatchers.IO) {
file.createNewFile()
}
}
file.writeText(JSON.encodeToString(database.map { it.copy(inventorySorting = null) }))
logger.info("Saved Database Player for ${data.uuid} to $file")
}
}
@@ -0,0 +1,12 @@
package gg.norisk.heroes.common.events
import net.minecraft.client.input.Input
import net.silkmc.silk.core.event.Event
open class AfterTickInputEvent(val input: Input)
open class MouseScrollEvent(val window: Long, val horizontal: Double, val vertical: Double)
val mouseScrollEvent = Event.onlySync<MouseScrollEvent>()
val afterTickInputEvent = Event.onlySync<AfterTickInputEvent>()
@@ -0,0 +1,46 @@
package gg.norisk.heroes.common.events
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.render.chunk.SectionBuilder
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.Entity
import net.minecraft.entity.LivingEntity
import net.minecraft.entity.data.DataTracker
import net.minecraft.entity.data.TrackedData
import net.silkmc.silk.core.event.Cancellable
import net.silkmc.silk.core.event.Event
import net.silkmc.silk.core.event.EventScopeProperty
object EntityEvents {
class EntityTrackedDataSetEvent(val entity: Entity, val data: TrackedData<*>)
open class LivingEntityEvent(val livingEntity: LivingEntity)
class InitDataTrackerEvent(livingEntity: LivingEntity, val dataTracker: DataTracker) :
LivingEntityEvent(livingEntity)
open class EntityRendererEvent(
val entity: Entity,
val f: Float,
val g: Float,
val matrixStack: MatrixStack,
val vertexConsumerProvider: VertexConsumerProvider,
val light: Int
) : Cancellable {
override val isCancelled = EventScopeProperty(false)
}
open class ComputeFallDamageEvent(
val fallDistance: Float,
val damageMultiplier: Float,
val originalFallDamage: Int,
livingEntity: LivingEntity
) : LivingEntityEvent(
livingEntity
) {
var fallDamage: Int? = null
}
val computeFallDamageEvent = Event.onlySync<ComputeFallDamageEvent>()
val onTrackedDataSetEvent = Event.onlySync<EntityTrackedDataSetEvent>()
val entityRendererEvent = Event.onlySync<EntityRendererEvent>()
val livingEntityTickMovementEvent = Event.onlySync<LivingEntityEvent>()
}
@@ -0,0 +1,30 @@
package gg.norisk.heroes.common.events
import gg.norisk.heroes.common.hero.Hero
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.server.network.ServerPlayerEntity
import net.silkmc.silk.core.annotations.ExperimentalSilkApi
import net.silkmc.silk.core.event.Cancellable
import net.silkmc.silk.core.event.Event
import net.silkmc.silk.core.event.EventScopeProperty
@OptIn(ExperimentalSilkApi::class)
object HeroEvents {
open class HeroChangeEvent(val player: PlayerEntity)
val heroChangeEvent = Event.onlySync<HeroChangeEvent>()
open class HeroSelectEvent(val player: PlayerEntity, val hero: Hero, var canSelect: Boolean = false)
val heroSelectEvent = Event.onlySync<HeroSelectEvent>()
open class PreKitEditorEvent(val player: ServerPlayerEntity) : Cancellable {
override val isCancelled: EventScopeProperty<Boolean> = EventScopeProperty(false)
}
val preKitEditorEvent = Event.onlySync<PreKitEditorEvent>()
open class HeroDeathEvent(val player: ServerPlayerEntity, var isValidDeath: Boolean)
val heroDeathEvent = Event.onlySync<HeroDeathEvent>()
}
@@ -0,0 +1,246 @@
package gg.norisk.heroes.common.ffa
import gg.norisk.heroes.common.HeroesManager.isServer
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.HeroesManager.prefix
import gg.norisk.heroes.common.HeroesManager.toId
import gg.norisk.heroes.common.player.InventorySorting.Companion.loadInventory
import gg.norisk.heroes.common.events.HeroEvents
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket
import gg.norisk.heroes.common.player.InventorySorting
import gg.norisk.heroes.common.player.InventorySorting.Companion.CURRENT_VERSION
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.heroes.common.utils.PlayStyle
import gg.norisk.heroes.server.database.player.PlayerProvider
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents
import net.minecraft.block.Blocks
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.network.packet.s2c.play.PositionFlag
import net.minecraft.particle.ItemStackParticleEffect
import net.minecraft.particle.ParticleTypes
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.server.world.ServerWorld
import net.minecraft.sound.SoundCategory
import net.minecraft.sound.SoundEvents
import net.minecraft.text.Text
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
import net.minecraft.world.GameMode
import net.silkmc.silk.core.task.mcCoroutineTask
import net.silkmc.silk.core.text.broadcastText
import net.silkmc.silk.core.text.literalText
import kotlin.math.cos
import kotlin.math.sin
object KitEditorManager {
var world: ServerWorld? = null
var resetInventory: (PlayerEntity) -> Unit = {
it.inventory.clear()
it.inventory.main.set(0, Items.STONE_SWORD.defaultStack)
}
var onBack: (ServerPlayerEntity) -> Unit = {
it.teleport(it.server.overworld, 0.0, 100.0, 0.0, PositionFlag.VALUES, 0f, 0f)
}
private val kitEditorSpawn = Vec3d(0.5, 90.5, 0.5)
val hasKitWorld get() = world != null
fun init() {
if (!isServer) return
ServerLifecycleEvents.SERVER_STARTED.register {
for (world in it.worlds) {
logger.info("Found Worlds: $world ${world.registryKey.value}")
if (world.registryKey.value == "kit-editor".toId()) {
this.world = world
break
}
}
logger.info("Found Kit Editor World $world")
}
ServerTickEvents.END_WORLD_TICK.register {
if (it == world) {
for (player in it.players) {
val distance = player.squaredDistanceTo(kitEditorSpawn)
//player.sendMessage("Distance: $distance".literal)
if (distance > 300) {
teleportToKitEditorSpawn(player)
}
}
}
}
ServerWorldEvents.LOAD.register(ServerWorldEvents.Load { server, world ->
if (world.registryKey.value == "kit-editor".toId()) {
this.world = world
this.world?.worldBorder?.size = 10000.0
server.broadcastText("LOADED KIT EDITOR WORLD")
server.broadcastText("LOADED KIT EDITOR WORLD")
/*/world.timeOfDay = 6000
world.gameRules.get(GameRules.DO_DAYLIGHT_CYCLE).set(false, server)
world.gameRules.get(GameRules.DO_WEATHER_CYCLE).set(false, server)
world.gameRules.get(GameRules.SPECTATORS_GENERATE_CHUNKS).set(false, server)
world.gameRules.get(GameRules.DO_MOB_SPAWNING).set(false, server)
world.gameRules.get(GameRules.DO_ENTITY_DROPS).set(false, server)*/
}
})
ServerEntityEvents.ENTITY_LOAD.register(ServerEntityEvents.Load { entity, world ->
val player = entity as? ServerPlayerEntity? ?: return@Load
if (world == this.world) {
player.changeGameMode(GameMode.ADVENTURE)
mcCoroutineTask(sync = false, client = false) {
val ffaPlayer = PlayerProvider.get(player.uuid)
println("Loaded ${ffaPlayer}")
if (ffaPlayer.inventorySorting == null) {
resetInventory.invoke(player)
ffaPlayer.inventorySorting = player.toDatabaseInventory()
}
mcCoroutineTask(sync = true, client = false) {
player.loadInventory(ffaPlayer.inventorySorting!!)
}
entity.sendMessage(Text.translatable("ffa.mechanic.kit.editor.enter"))
}
}
})
ServerEntityEvents.ENTITY_UNLOAD.register(ServerEntityEvents.Unload { entity, world ->
val player = entity as? ServerPlayerEntity? ?: return@Unload
if (world == this.world) {
val inventory = player.toDatabaseInventory()
mcCoroutineTask(sync = false, client = true) {
val ffaPlayer = PlayerProvider.get(player.uuid)
ffaPlayer.inventorySorting = inventory
player.ffaPlayer = ffaPlayer
mcCoroutineTask(sync = false, client = false) {
PlayerProvider.save(ffaPlayer)
println("Saved ${ffaPlayer}")
entity.sendMessage(Text.translatable("ffa.mechanic.kit.editor.save"))
}
entity.sendMessage(Text.translatable("ffa.mechanic.kit.editor.left"))
mcCoroutineTask(sync = true, client = false) {
player.loadInventory(inventory)
}
}
}
})
Networking.c2sKitEditorRequestPacket.receiveOnServer { packet, context ->
mcCoroutineTask(sync = true, client = false) {
val player = context.player
val kitEditorWorld = world ?: return@mcCoroutineTask
val event = HeroEvents.PreKitEditorEvent(player)
HeroEvents.preKitEditorEvent.invoke(event)
if (!event.isCancelled.get()) {
Networking.s2cHeroSelectorPacket.send(
HeroSelectorPacket(
emptyList(),
false,
hasKitWorld
), player
)
teleportToKitEditorSpawn(player)
player.sendMessage(literalText {
text(prefix)
text(Text.translatable("ffa.mechanic.kit.editor.inventory_instruction"))
})
kitEditorWorld.setBlockState(BlockPos(0, 89, 0), Blocks.GOLD_BLOCK.defaultState)
}
}
}
}
fun onBack(player: ServerPlayerEntity) {
onBack.invoke(player)
}
fun onReset(player: ServerPlayerEntity) {
resetInventory.invoke(player)
}
private fun teleportToKitEditorSpawn(player: ServerPlayerEntity) {
player.teleport(
world,
kitEditorSpawn.x,
kitEditorSpawn.y,
kitEditorSpawn.z,
PositionFlag.VALUES,
0f,
0f
)
player.playSoundToPlayer(SoundEvents.ENTITY_ENDERMAN_TELEPORT, SoundCategory.PLAYERS, 0.3f, 1f)
player.serverWorld.syncWorldEvent(2003, player.blockPos, 0)
}
private fun spawnEnderEyeBreak(blockPos: BlockPos, player: ServerPlayerEntity) {
val d: Double = blockPos.getX().toDouble() + 0.5
val e: Double = blockPos.getY().toDouble()
val f: Double = blockPos.getZ().toDouble() + 0.5
for (k in 0..7) {
player.serverWorld.spawnParticles(
player,
ItemStackParticleEffect(ParticleTypes.ITEM, ItemStack(Items.ENDER_EYE)),
false,
d,
e,
f,
1,
player.world.random.nextGaussian() * 0.15,
player.world.random.nextDouble() * 0.2,
player.world.random.nextGaussian() * 0.15,
0.0
)
}
var g = 0.0
while (g < Math.PI * 2) {
player.serverWorld.spawnParticles(
player,
ParticleTypes.PORTAL,
false,
d + cos(g) * 5.0,
e - 0.4,
f + sin(g) * 5.0,
1,
cos(g) * -5.0,
0.0,
sin(g) * -5.0,
0.0
)
player.serverWorld.spawnParticles(
player,
ParticleTypes.PORTAL,
false,
d + cos(g) * 5.0,
e - 0.4,
f + sin(g) * 5.0,
1,
cos(g) * -5.0,
0.0,
sin(g) * -5.0,
0.0
)
g += Math.PI / 20
}
}
private fun ServerPlayerEntity.toDatabaseInventory(): InventorySorting {
return InventorySorting(
uuid,
PlayStyle.current,
CURRENT_VERSION,
inventory.armor.toTypedArray(),
inventory.offHand.toTypedArray(),
inventory.main.toTypedArray(),
)
}
}
@@ -0,0 +1,79 @@
package gg.norisk.heroes.common.ffa.experience
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.HeroesManager.prefix
import gg.norisk.heroes.common.player.ffaPlayer
import gg.norisk.heroes.common.utils.createIfNotExists
import gg.norisk.heroes.server.config.ConfigManagerServer.JSON
import gg.norisk.heroes.server.database.player.PlayerProvider
import kotlinx.serialization.encodeToString
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.server.network.ServerPlayerEntity
import net.silkmc.silk.core.task.mcCoroutineTask
import net.silkmc.silk.core.text.literalText
import java.awt.Color
object Experience {
private val configFile = HeroesManager.baseDirectory.resolve("xp-config.json").createIfNotExists()
fun init() {
loadConfig()
}
fun add(player: ServerPlayerEntity, reason: ExperienceReason, printMessage: Boolean = false) {
mcCoroutineTask(sync = false, client = false) {
val receivedXp = reason.value
val ffaPlayer = PlayerProvider.get(player.uuid)
ffaPlayer.xp += receivedXp
player.ffaPlayer = ffaPlayer
if (printMessage) {
player.sendMessage(literalText {
text(prefix)
text("+$receivedXp XP") {
color = Color.GREEN.rgb
}
})
}
PlayerProvider.save(ffaPlayer)
}
}
private fun loadConfig() {
val currentConfig = loadFromFile()
createDefaultConfig(currentConfig.isEmpty())
currentConfig.forEach { configReason ->
val reason = ExperienceRegistry.reasons.firstOrNull { reason -> reason.key == configReason.key }
if (reason == null) {
logger.warn("Found invalid reason with key `${configReason.key}` in config")
return@forEach
}
reason.value = configReason.value
}
}
private fun loadFromFile(): MutableSet<ExperienceReason> {
return runCatching<MutableSet<ExperienceReason>> {
JSON.decodeFromString(configFile.readText())
}.onFailure {
it.printStackTrace()
}.onSuccess {
logger.info("Loaded Xp Config")
}.getOrDefault(mutableSetOf())
}
private fun createDefaultConfig(force: Boolean) {
if (force) {
configFile.createNewFile()
configFile.writeText(JSON.encodeToString(ExperienceRegistry.reasons))
logger.info("Created Default Xp Config")
}
}
}
fun PlayerEntity.addXp(reason: ExperienceReason, printMessage: Boolean = false) {
Experience.add(this as ServerPlayerEntity, reason, printMessage)
}
@@ -0,0 +1,6 @@
package gg.norisk.heroes.common.ffa.experience
import kotlinx.serialization.Serializable
@Serializable
data class ExperienceReason(val key: String, var value: Int)
@@ -0,0 +1,21 @@
package gg.norisk.heroes.common.ffa.experience
object ExperienceRegistry {
val reasons = mutableSetOf<ExperienceReason>()
val KILLED_PLAYER = register("killed_player", 200)
val PLAYER_DEATH = register("player_death", 25)
val SOUP_EATEN = register("soup_eaten", 5)
val SMALL_ABILITY_USE = register("small_ability_use", 5)
val RECRAFT = register("soup_recraft", 5)
val END_KILL_STREAK = register("end_kill_streak", 1000)
val DEALING_DAMAGE = register("dealing_damage", 1)
val TAKING_DAMAGE = register("taking_damage", 1)
val IDLE = register("idle", 1)
fun register(key: String, value: Int): ExperienceReason {
return ExperienceReason(key, value).apply {
reasons.add(this)
}
}
}
@@ -0,0 +1,100 @@
package gg.norisk.heroes.common.hero
import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.HeroesManager.toId
import gg.norisk.heroes.common.ability.PlayerProperty.Companion.JSON
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import java.io.File
open class Hero(val name: String) {
companion object {
/**
* Creates a new lazy hero delegate.
*
* @param config the config of this hero
* @param builder the [HeroBuilder]
*/
inline operator fun invoke(
name: String,
crossinline builder: HeroBuilder.() -> Unit
) = lazy {
Hero(name).apply {
HeroBuilder(this).apply(builder)
}
}
}
var internalCallbacks = InternalCallbacks()
val internalKey = name.lowercase().replace(' ', '_')
val icon = "textures/hero/${internalKey}/icon.png".toId()
val description = Text.translatable("text.hero.$internalKey.description")
var overlaySkin: Identifier? = null
val abilities = hashMapOf<String, AbstractAbility<*>>()
var color: Int = 0x4291AD
fun registerAbility(ability: AbstractAbility<*>) {
ability.hero = this
// REMOVED AbilityKeyBindManager.initializeKeyBind(ability)
abilities[ability.internalKey] = ability
}
fun getUsableAbilities(player: PlayerEntity): List<AbstractAbility<*>> {
return abilities.values.toList()
}
@Serializable
data class HeroJson(
val internalKey: String,
val properties: Map<String, JsonArray>
)
fun load(heroJson: HeroJson? = null) {
if (baseFile.exists()) {
runCatching {
val loaded = heroJson ?: JSON.decodeFromString<HeroJson>(baseFile.readText())
for ((key, element) in loaded.properties) {
val ability = abilities[key]
for (jsonElement in element) {
val name = jsonElement.jsonObject["name"] ?: continue
val property = ability?.getAllProperties()?.find { it.name == name.jsonPrimitive.content }
property?.fromJson(jsonElement.toString())
}
}
}.onFailure {
logger.error("Error Loading Hero $internalKey ${it.message}")
it.printStackTrace()
}
}
}
private val baseFolder get() = File(HeroesManager.baseDirectory, "heroes/hero").apply { mkdirs() }
private val baseFile get() = File(baseFolder, "$internalKey.json")
fun save() {
baseFile.writeText(JSON.encodeToString(toHeroJson()))
logger.info("Successfully saved $internalKey")
}
fun toHeroJson(): HeroJson {
val properties = buildMap {
for ((key, ability) in abilities) {
put(key, JsonArray(ability.getAllProperties().map { it.toJsonElement() }))
}
}
return HeroJson(internalKey, properties)
}
inner class InternalCallbacks {
var getSkin: ((player: PlayerEntity) -> Identifier?)? = null
}
}
@@ -0,0 +1,57 @@
package gg.norisk.heroes.common.hero
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.util.Identifier
import net.silkmc.silk.core.event.Event
import net.silkmc.silk.core.event.EventPriority
import net.silkmc.silk.core.event.MutableEventScope
class HeroBuilder(val hero: Hero) {
var color: Int
get() = hero.color
set(value) {
hero.color = value
}
var overlaySkin: Identifier?
get() = hero.overlaySkin
set(value) {
hero.overlaySkin = value
}
/**
* Executes the given [callback] if the player of the
* [playerGetter] is the hero.
*/
inline fun <reified T> Event<T>.heroPlayerEvent(
crossinline playerGetter: (T) -> PlayerEntity?,
priority: EventPriority = EventPriority.NORMAL,
crossinline callback: context(MutableEventScope) (event: T) -> Unit
) {
this.listen(priority) {
val player = playerGetter(it) ?: return@listen
if (player.getHero() != hero) return@listen
callback.invoke(MutableEventScope, it)
}
}
/**
* Executes the given [callback] when the event is called
*/
inline fun <reified T> Event<T>.heroEvent(
priority: EventPriority = EventPriority.NORMAL,
crossinline callback: context(MutableEventScope) (event: T) -> Unit
) {
this.listen(priority) {
callback.invoke(MutableEventScope, it)
}
}
fun getSkin(callback: (player: PlayerEntity) -> Identifier) {
hero.internalCallbacks.getSkin = callback
}
fun ability(ability: AbstractAbility<*>) {
hero.registerAbility(ability)
}
}
@@ -0,0 +1,64 @@
package gg.norisk.heroes.common.hero
import gg.norisk.datatracker.entity.getSyncedData
import gg.norisk.datatracker.entity.setSyncedData
import gg.norisk.heroes.common.HeroesManager.isClient
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.hero.HeroManager.HERO_KEY
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import gg.norisk.heroes.common.hero.ability.task.AbilityCoroutineManager
import gg.norisk.heroes.server.config.ConfigManagerServer
import gg.norisk.heroes.server.hero.ability.AbilityManagerServer
import net.minecraft.client.MinecraftClient
import net.minecraft.entity.player.PlayerEntity
import net.silkmc.silk.core.Silk.server
import net.silkmc.silk.core.text.broadcastText
object HeroManager {
val registeredHeroes: MutableMap<String, Hero> = mutableMapOf()
const val HERO_KEY = "hero"
fun getHero(internalKey: String) = registeredHeroes[internalKey.replace(' ', '_')]
fun registerHero(hero: Hero): Boolean {
logger.info("Register Hero ${hero.name}... on $this")
registeredHeroes[hero.internalKey] = hero
hero.abilities.values.forEach(AbstractAbility<*>::init)
return true
}
fun reloadHeroes(vararg heroes: Hero) {
for (hero in heroes) {
runCatching {
hero.load()
hero.save()
ConfigManagerServer.sendHeroSettings(hero)
}.onSuccess {
server?.broadcastText("Loaded Hero ${hero.name}")
}.onFailure {
server?.broadcastText("Error loading Hero ${hero.name}")
}
}
}
}
fun PlayerEntity.setHero(hero: Hero?) {
if (isClient && this == MinecraftClient.getInstance().player) {
AbilityCoroutineManager.cancelClientJobs()
} else {
AbilityCoroutineManager.cancelServerJobs(this)
AbilityManagerServer.clear(this)
}
getHero()?.abilities?.forEach { (name, ability) ->
ability.clearCooldown(this)
ability.onDisable(this)
}
this.setSyncedData(HERO_KEY, hero?.internalKey ?: "NONE")
getHero()?.abilities?.forEach { (name, ability) -> ability.onEnable(this) }
}
fun PlayerEntity.getHero(): Hero? {
return HeroManager.getHero(this.getSyncedData<String>(HERO_KEY) ?: "NONE")
}
fun PlayerEntity.isHero(hero: Hero?) = this.getHero() == hero
@@ -0,0 +1,33 @@
package gg.norisk.heroes.common.hero.ability
import gg.norisk.heroes.common.serialization.UUIDSerializer
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable
data class AbilityPacket<C : AbilityPacketDescription>(
@Serializable(with = UUIDSerializer::class)
val playerUuid: UUID,
val heroKey: String,
val abilityKey: String,
val description: C
)
@Serializable
data class SkillPropertyPacket(
val heroKey: String,
val abilityKey: String,
val propertyKey: String
)
@Serializable
sealed class AbilityPacketDescription {
@Serializable
object Start : AbilityPacketDescription()
@Serializable
open class Use : AbilityPacketDescription()
@Serializable
object End : AbilityPacketDescription()
}
@@ -0,0 +1,24 @@
package gg.norisk.heroes.common.hero.ability
import net.minecraft.entity.player.PlayerEntity
class AbilityScope(val executingPlayer: PlayerEntity) {
var applyCooldown = true
var broadcastPacket = false
fun cancelCooldown() {
applyCooldown = false
}
fun applyCooldown() {
applyCooldown = true
}
fun cancelBroadcasting() {
broadcastPacket = false
}
fun broadcast() {
broadcastPacket = true
}
}
@@ -0,0 +1,279 @@
package gg.norisk.heroes.common.hero.ability
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.ability.*
import gg.norisk.heroes.common.ability.operation.AddValueTotal
import gg.norisk.heroes.common.ability.operation.MultiplyBase
import gg.norisk.heroes.common.ability.operation.Operation
import gg.norisk.heroes.common.cooldown.CooldownInfo
import gg.norisk.heroes.common.cooldown.MultipleUsesInfo
import gg.norisk.heroes.common.ffa.experience.ExperienceRegistry
import gg.norisk.heroes.common.ffa.experience.addXp
import gg.norisk.heroes.common.hero.Hero
import gg.norisk.heroes.common.networking.Networking
import gg.norisk.heroes.server.config.ConfigManagerServer.JSON
import gg.norisk.utils.DevUtils.uniqueId
import io.wispforest.owo.ui.component.Components
import io.wispforest.owo.ui.core.Component
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import net.fabricmc.api.EnvType
import net.fabricmc.api.Environment
import net.minecraft.client.option.KeyBinding
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.Items
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.silkmc.silk.core.task.mcCoroutineTask
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
abstract class AbstractAbility<T : Any>(val name: String) {
lateinit var hero: Hero
val internalKey = name.lowercase().replace(' ', '_')
val description by lazy { Text.translatable("hero.${hero.internalKey}.ability.$internalKey.description") }
var condition: ((PlayerEntity) -> Boolean)? = null
var showInKeybindHud: Boolean = true
@Environment(EnvType.CLIENT)
var keyBind: KeyBinding? = null
var properties = listOf<PlayerProperty<*>>()
private val cooldowns = ConcurrentHashMap<UUID, CooldownInfo>()
private val cooldownTasks = ConcurrentHashMap<UUID, Job>()
var cooldownProperty: CooldownProperty = buildCooldown(5.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0))
var usageProperty: AbstractUsageProperty = SingleUseProperty(0.0, 0, "Use", MultiplyBase(listOf(0.0))).apply {
icon = {
Components.item(Items.STONE_PICKAXE.defaultStack)
}
}
//atm used for holdcooldown
open val extraProperties: List<PlayerProperty<*>> = emptyList()
private var allProperties: List<PlayerProperty<*>>? = null
fun getAllProperties(): List<PlayerProperty<*>> {
//Todo das cachen?
return buildList {
add(cooldownProperty)
add(usageProperty)
addAll(extraProperties)
addAll(properties)
}
}
open fun getCustomActivation(): Text {
return Text.translatable("heroes.ability.$internalKey.custom_activation")
}
open fun hasUnlocked(player: PlayerEntity): Boolean {
return true
}
/*
ähnlich wie condition aber nur ServerSide Condition Check
*/
open fun canUse(player: ServerPlayerEntity): Boolean {
return true
}
open fun getUnlockCondition(): Text {
return Text.empty()
}
open fun getIconComponent(): Component {
return Components.item(Items.DIAMOND_SWORD.defaultStack)
}
open fun getBackgroundTexture(): Identifier {
return Identifier.of("textures/block/stone.png")
}
fun handleCooldown(player: ServerPlayerEntity): Boolean {
if (hasCooldown(player)) {
// Client an den cooldown erinnern!
getCooldown(player)?.let { cooldownInfo ->
//player.sendDebugMessage("Cooldown: ${cooldownInfo.remaining}".literal)
Networking.s2cCooldownPacket.send(cooldownInfo, player)
return true
}
}
return false
}
fun removeCooldown(player: PlayerEntity) {
cooldowns.remove(player.uuid)
}
fun setCooldown(cooldownInfo: CooldownInfo, player: PlayerEntity) {
if (cooldownInfo.duration == 0L && cooldownInfo.startTime == 0L && cooldownInfo.currentTime == 0L) {
cooldowns.remove(player.uuid)
} else {
cooldowns[player.uuid] = cooldownInfo
}
}
fun clearCooldown(player: PlayerEntity) {
cooldownTasks[player.uuid]?.cancel()
if (cooldowns.remove(player.uuid) != null) {
if (player is ServerPlayerEntity) {
Networking.s2cCooldownPacket.send(
CooldownInfo(
player.id,
0,
0,
0,
null,
hero.internalKey,
internalKey,
null
), player
)
}
}
}
fun addCooldown(player: PlayerEntity) {
if (player !is ServerPlayerEntity) return
if (cooldownProperty.name == "NoCooldown") return
//has cooldown
if (handleCooldown(player)) return
val uuid = player.uuid
val currentTime = System.nanoTime()
var startTime: Long? = null
var multipleUsesInfo: MultipleUsesInfo? = null
if (usageProperty is MultiUseProperty) {
val currentUse = (usageProperty as MultiUseProperty).uses.getOrDefault(player.uuid, 0) + 1
(usageProperty as MultiUseProperty).uses[player.uuid] = currentUse
val maxUses = usageProperty.getValue(player.uuid).toInt()
multipleUsesInfo = MultipleUsesInfo(currentUse, maxUses)
if (currentUse == maxUses) {
startTime = currentTime
(usageProperty as MultiUseProperty).uses[player.uuid] = 0
}
} else if (usageProperty is SingleUseProperty) {
startTime = currentTime
}
val value = cooldownProperty.getValue(player.uuid)
logger.info("Sending Cooldown $value to ${player.gameProfile.name}")
//player.sendDebugMessage("Value: $value Level: ${cooldownProperty.getLevelInfo(player.uuid)}".literal)
//player.sendDebugMessage("Property: $cooldownProperty".literal)
val duration = value.seconds.inWholeNanoseconds
val cooldownInfo = CooldownInfo(
player.id,
duration,
startTime,
currentTime,
multipleUsesInfo,
hero.internalKey,
internalKey,
startTime?.let { it + duration }
).apply {
this.durationString = getCooldownText(this)
}
player.addXp(ExperienceRegistry.SMALL_ABILITY_USE, true)
cooldowns[uuid] = cooldownInfo
Networking.s2cCooldownPacket.send(cooldownInfo, player)
//player.sendDebugMessage("Sending Cooldown: $cooldownInfo".literal)
if (cooldownInfo.remaining > 0) {
cooldownTasks[uuid]?.cancel()
cooldownTasks[uuid] = mcCoroutineTask(sync = false, client = false) {
//NO DELAY IN CREATIVE MODE FOR TESTING?
//player.sendMessage("START".literal.withColor(Color.red.rgb))
//player.sendMessage(getCooldownText(cooldownInfo)?.literal)
if (!player.isCreative) {
delay(value.seconds.inWholeMilliseconds)
}
//player.sendMessage("END".literal.withColor(Color.red.rgb))
//player.sendMessage(getCooldownText(cooldownInfo)?.literal)
cooldowns -= uuid
Networking.s2cCooldownPacket.send(
CooldownInfo(
player.id,
0,
0,
0,
multipleUsesInfo,
hero.internalKey,
internalKey,
null
), player
)
}
}
}
fun getCooldown(player: PlayerEntity): CooldownInfo? {
return cooldowns[player.uuid]
}
fun hasCooldown(player: PlayerEntity): Boolean {
val cooldownInfo = getCooldown(player) ?: return false
//player.sendDebugMessage("Cooldown: $cooldownInfo".literal)
return cooldownInfo.remaining > 0
}
fun init() {
for (property in getAllProperties()) {
property.ability = this
property.hero = this.hero
}
}
open fun onEnable(player: PlayerEntity) {
}
open fun onDisable(player: PlayerEntity) {
}
open fun onStart(player: PlayerEntity, abilityScope: AbilityScope) {
}
open fun onTick(player: PlayerEntity) {
}
protected fun buildMultipleUses(baseValue: Double, maxLevel: Int, operation: Operation): MultiUseProperty {
return MultiUseProperty(baseValue, maxLevel, "Use", operation)
}
protected fun buildCooldown(baseValue: Double, maxLevel: Int, operation: Operation): CooldownProperty {
return CooldownProperty(baseValue, maxLevel, "Cooldown", operation)
}
protected fun buildNoCooldown(): CooldownProperty {
return CooldownProperty(0.0, 0, "NoCooldown", MultiplyBase())
}
fun getCooldownText(cooldown: CooldownInfo): String? {
val remaining = cooldown.remaining
if (remaining > 0) {
val builder = StringBuilder()
remaining.nanoseconds.toComponents { days, hours, minutes, seconds, nanoseconds ->
if (days > 0) builder.append(days).append("d ")
if (hours > 0) builder.append(hours).append("h ")
if (minutes > 0) builder.append(minutes).append("m ")
builder.append(seconds).append(".")
builder.append((nanoseconds / 1_000_000).toString().padStart(3, '0').take(1)) // Nur 2 Stellen
}
return builder.toString()
}
if (usageProperty is MultiUseProperty) {
val multipleUseInfo = cooldown.multipleUsesInfo ?: return null
val currentUse = multipleUseInfo.currentUse
val maxUses = multipleUseInfo.maxUses
val remainingUses = maxUses - currentUse
return "$remainingUses/$maxUses"
}
return null
}
}
@@ -0,0 +1,21 @@
package gg.norisk.heroes.common.hero.ability
import gg.norisk.heroes.common.hero.Hero
import net.minecraft.entity.player.PlayerEntity
interface IAbilityManager {
fun init()
fun useAbility(
player: PlayerEntity,
hero: Hero,
ability: AbstractAbility<*>,
description: AbilityPacketDescription.Use
): Boolean
fun useAbility(player: PlayerEntity, ability: AbstractAbility<*>, description: AbilityPacketDescription.Use)
fun registerAbility(ability: AbstractAbility<*>)
fun isUsingAbility(player: PlayerEntity, ability: AbstractAbility<*>): Boolean
}
@@ -0,0 +1,6 @@
package gg.norisk.heroes.common.hero.ability.implementation
import gg.norisk.heroes.common.hero.ability.AbstractAbility
class Ability<C : Any>(name: String) : AbstractAbility<C>(name) {
}
@@ -0,0 +1,4 @@
package gg.norisk.heroes.common.hero.ability.implementation
open class HoldAbility(name: String) : ToggleAbility(name) {
}
@@ -0,0 +1,6 @@
package gg.norisk.heroes.common.hero.ability.implementation
import gg.norisk.heroes.common.hero.ability.AbstractAbility
open class PressAbility(name: String) : AbstractAbility<Any>(name) {
}
@@ -0,0 +1,38 @@
package gg.norisk.heroes.common.hero.ability.implementation
import gg.norisk.heroes.common.ability.CooldownProperty
import gg.norisk.heroes.common.ability.PlayerProperty
import gg.norisk.heroes.common.ability.operation.AddValueTotal
import gg.norisk.heroes.common.ability.operation.Operation
import gg.norisk.heroes.common.hero.ability.AbilityScope
import gg.norisk.heroes.common.hero.ability.AbstractAbility
import net.minecraft.entity.player.PlayerEntity
open class ToggleAbility(name: String) : AbstractAbility<Any>(name) {
override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) {
}
open fun onUse(player: PlayerEntity) {
}
open fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) {
}
var maxDurationProperty = buildMaxDuration(10.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0))
data class AbilityEndInformation(var applyCooldown: Boolean = true)
protected fun buildMaxDuration(baseValue: Double, maxLevel: Int, operation: Operation): CooldownProperty {
return CooldownProperty(
baseValue, maxLevel,
"Max Duration",
operation
)
}
override val extraProperties: List<PlayerProperty<*>>
get() = listOf(maxDurationProperty)
}
@@ -0,0 +1,54 @@
package gg.norisk.heroes.common.hero.ability.task
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.hero.ability.AbilityScope
import kotlinx.coroutines.*
import net.minecraft.client.MinecraftClient
import net.minecraft.entity.player.PlayerEntity
import net.silkmc.silk.core.annotations.DelicateSilkApi
import net.silkmc.silk.core.kotlin.ticks
import net.silkmc.silk.core.task.*
import java.util.*
import kotlin.time.Duration
object AbilityCoroutineManager {
val playerJobs = hashMapOf<UUID, MutableList<Job>>()
fun cancelServerJobs(player: PlayerEntity) {
playerJobs[player.uuid]?.forEach(Job::cancel)
playerJobs.remove(player.uuid)
}
fun cancelClientJobs() {
val uuid = playerJobs.keys.firstOrNull()
playerJobs[uuid]?.forEach(Job::cancel)
playerJobs.remove(uuid)
}
}
@OptIn(DelicateSilkApi::class)
inline fun abilityCoroutineTask(
executingPlayer: PlayerEntity,
sync: Boolean = true,
client: Boolean = false,
scope: CoroutineScope = if (sync) {
if (client) mcClientCoroutineScope else mcCoroutineScope
} else silkCoroutineScope,
howOften: Long = 1,
period: Duration = 1.ticks,
delay: Duration = Duration.ZERO,
crossinline task: suspend CoroutineScope.(task: CoroutineTask) -> Unit
): Job {
val uuid = if (client) MinecraftClient.getInstance().player!!.uuid else executingPlayer.uuid
return mcCoroutineTask(sync, client, scope, howOften, period, delay) { coroutineTask ->
if (isActive && !executingPlayer.isAlive) {
logger.info("${executingPlayer.name.literalString} is currently dead, cancelling coroutine job")
cancel()
}
ensureActive()
task(this, coroutineTask)
AbilityCoroutineManager.playerJobs[uuid]?.remove(this.coroutineContext.job)
}.also { job ->
AbilityCoroutineManager.playerJobs.computeIfAbsent(uuid) { mutableListOf() }.add(job)
}
}
@@ -0,0 +1,53 @@
package gg.norisk.heroes.common.hero.utils
object ColorUtils {
fun hexAsRgb(hexColor: Int): Triple<Int, Int, Int> {
val red = (hexColor shr 16) and 0xFF
val green = (hexColor shr 8) and 0xFF
val blue = hexColor and 0xFF
return Triple(red, green, blue)
}
fun isLightColor(hexColor: Int): Boolean {
val (red, green, blue) = hexAsRgb(hexColor)
// Calculate luminance
val luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
// Determine if color is light or dark
return luminance > 0.5
}
fun isDarkColor(hexColor: Int) = !isLightColor(hexColor)
fun darkenHexColor(hexColor: Int, factor: Double): Int {
// Extract RGB components
val (red, green, blue) = hexAsRgb(hexColor)
// Darken each component
val darkenedRed = (red * (1 - factor)).toInt()
val darkenedGreen = (green * (1 - factor)).toInt()
val darkenedBlue = (blue * (1 - factor)).toInt()
// Combine components and return the darkened color
return (darkenedRed shl 16) or (darkenedGreen shl 8) or darkenedBlue
}
fun lightenHexColor(hexColor: Int, factor: Double): Int {
// Extract RGB components
val (red, green, blue) = hexAsRgb(hexColor)
// Darken each component
val lightenedRed = (red + (255 - red) * factor).toInt()
val lightenedGreen = (green + (255 - green) * factor).toInt()
val lightenedBlue = (blue + (255 - blue) * factor).toInt()
// Combine components and return the darkened color
return (lightenedRed shl 16) or (lightenedGreen shl 8) or lightenedBlue
}
fun contrast(hexColor: Int): Int {
return if (isDarkColor(hexColor)) lightenHexColor(hexColor, 0.33)
else darkenHexColor(hexColor, 0.33)
}
}
@@ -0,0 +1,24 @@
package gg.norisk.heroes.common.networking
import gg.norisk.heroes.common.HeroesManager.toId
import kotlinx.serialization.Serializable
import net.silkmc.silk.network.packet.s2cPacket
interface CameraShakeEvent {
fun isValid(t: Double): Boolean
fun getCameraShakeMagnitude(t: Double): Double
}
@Serializable
data class BoomShake(private var magnitude: Double, private var sustain: Double, private var fade: Double) :
CameraShakeEvent {
override fun isValid(t: Double): Boolean = t < sustain + fade
override fun getCameraShakeMagnitude(t: Double): Double {
return when {
t <= sustain -> magnitude
else -> magnitude * (1 - (t - sustain) / fade)
}
}
}
val cameraShakePacket = s2cPacket<BoomShake>("camera-shake".toId())
@@ -0,0 +1,30 @@
package gg.norisk.heroes.common.networking
import gg.norisk.heroes.common.HeroesManager.toId
import gg.norisk.heroes.common.cooldown.CooldownInfo
import gg.norisk.heroes.common.hero.ability.AbilityPacket
import gg.norisk.heroes.common.hero.ability.AbilityPacketDescription
import gg.norisk.heroes.common.hero.ability.SkillPropertyPacket
import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket
import gg.norisk.heroes.common.networking.dto.MousePacket
import net.silkmc.silk.network.packet.c2sPacket
import net.silkmc.silk.network.packet.s2cPacket
object Networking {
val c2sAbilityPacket = c2sPacket<AbilityPacket<out AbilityPacketDescription>>("use-ability".toId())
val s2cAbilityPacket = s2cPacket<AbilityPacket<out AbilityPacketDescription>>("use-ability".toId())
val c2sSkillProperty = c2sPacket<SkillPropertyPacket>("skill-property".toId())
val s2cHeroSelectorPacket = s2cPacket<HeroSelectorPacket>("hero-selector-s2c".toId())
val c2sHeroSelectorPacket = c2sPacket<String>("hero-selector-c2s".toId())
val c2sKitEditorRequestPacket = c2sPacket<Unit>("kit-editor-request".toId())
val s2cCooldownPacket = s2cPacket<CooldownInfo>("cooldown".toId())
val mousePacket = c2sPacket<MousePacket>("mouse-packet".toId())
val mouseScrollPacket = c2sPacket<Boolean>("mouse-scroll".toId())
//warum String?
// java.lang.IllegalStateException: This serializer can be used only with Json format.Expected Encoder to be JsonEncoder, got class kotlinx.serialization.cbor.internal.CborMapWriter
val s2cHeroSettingsPacket = s2cPacket<String>("hero-settings".toId())
}
@@ -0,0 +1,141 @@
package gg.norisk.heroes.common.networking.dto
import it.unimi.dsi.fastutil.doubles.Double2DoubleFunction
import java.time.Duration
import kotlin.math.*
class AnimationInterpolator(val start: Float, val end: Float, var dur: Duration) {
var startTime: Long
var easing = Easing.LINEAR
val forward = true
init {
this.startTime = System.nanoTime()
}
constructor(start: Float, end: Float, duration: Duration, easing: Easing) : this(start, end, duration) {
this.easing = easing
}
fun setDuration(dur: Duration) {
this.dur = dur
}
fun reset() {
this.startTime = System.nanoTime()
}
fun get(): Float {
val currentTime = System.nanoTime()
val delta = currentTime - startTime
val nanoDuration = dur.toNanos()
var animDelta = delta.toFloat() / nanoDuration
animDelta = max(0.0, min(1.0, animDelta.toDouble())).toFloat()
if (!forward) {
animDelta = (1 - animDelta).toFloat()
}
animDelta = easing.apply(animDelta.toDouble())
return start + (end - start) * animDelta
}
val isDone: Boolean
get() = System.nanoTime() - startTime >= dur.toNanos()
enum class Easing(val floatFunction: Double2DoubleFunction) {
LINEAR(Double2DoubleFunction { x: Double -> x }),
SINE_IN(Double2DoubleFunction { x: Double -> 1 - cos(x * Math.PI / 2) }),
SINE_OUT(Double2DoubleFunction { x: Double -> sin(x * Math.PI / 2) }),
SINE_IN_OUT(Double2DoubleFunction { x: Double -> -(cos(Math.PI * x) - 1) / 2 }),
CUBIC_IN(Double2DoubleFunction { x: Double -> x.pow(3.0) }),
CUBIC_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x).pow(3.0) }),
CUBIC_IN_OUT(Double2DoubleFunction { x: Double -> if (x < 0.5) 4 * x * x * x else 1 - (-2 * x + 2).pow(3.0) / 2 }),
QUINT_IN(Double2DoubleFunction { x: Double -> x.pow(5.0) }),
QUINT_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x).pow(5.0) }),
QUINT_IN_OUT(Double2DoubleFunction { x: Double ->
if (x < 0.5) 16 * x * x * x * x * x else 1 - (-2 * x + 2).pow(
5.0
) / 2
}),
CIRC_IN(Double2DoubleFunction { x: Double -> 1 - sqrt(1 - x.pow(2.0)) }),
CIRC_OUT(Double2DoubleFunction { x: Double -> sqrt(1 - (x - 1).pow(2.0)) }),
CIRC_IN_OUT(Double2DoubleFunction { x: Double ->
if (x < 0.5) (1 - sqrt(1 - (2 * x).pow(2.0))) / 2 else (sqrt(
1 - (-2 * x + 2).pow(2.0)
) + 1) / 2
}),
ELASTIC_IN(Double2DoubleFunction { x: Double ->
val c4 = 2 * Math.PI / 3
if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else -2.0.pow(10 * x - 10) * sin((x * 10 - 10.75) * c4)
}),
ELASTIC_OUT(Double2DoubleFunction { x: Double ->
val c4 = 2 * Math.PI / 3
if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else 2.0.pow(-10 * x) * sin((x * 10 - 0.75) * c4) + 1
}),
ELASTIC_IN_OUT(Double2DoubleFunction { x: Double ->
val c5 = 2 * Math.PI / 4.5
val sin = sin((20 * x - 11.125) * c5)
if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else if (x < 0.5) -(2.0.pow(20 * x - 10) * sin) / 2 else 2.0.pow(-20 * x + 10) * sin / 2 + 1
}),
QUAD_IN(Double2DoubleFunction { x: Double -> x * x }),
QUAD_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x) * (1 - x) }),
QUAD_IN_OUT(Double2DoubleFunction { x: Double -> if (x < 0.5) 2 * x * x else 1 - (-2 * x + 2).pow(2.0) / 2 }),
QUART_IN(Double2DoubleFunction { x: Double -> x * x * x * x }),
QUART_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x).pow(4.0) }),
QUART_IN_OUT(Double2DoubleFunction { x: Double -> if (x < 0.5) 8 * x * x * x * x else 1 - (-2 * x + 2).pow(4.0) / 2 }),
EXPO_IN(Double2DoubleFunction { x: Double -> if (x == 0.0) 0.0 else 2.0.pow(10 * x - 10) }),
EXPO_OUT(Double2DoubleFunction { x: Double -> if (x == 1.0) 1.0 else 1 - 2.0.pow(-10 * x) }),
EXPO_IN_OUT(Double2DoubleFunction { x: Double ->
if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else if (x < 0.5) 2.0.pow(
20 * x - 10
) / 2 else (2 - 2.0.pow(-20 * x + 10)) / 2
}),
BACK_IN(Double2DoubleFunction { x: Double ->
val c1 = 1.70158
val c3 = c1 + 1
c3 * x * x * x - c1 * x * x
}),
BACK_OUT(Double2DoubleFunction { x: Double ->
val c1 = 1.70158
val c3 = c1 + 1
1 + c3 * (x - 1).pow(3.0) + c1 * (x - 1).pow(2.0)
}),
BACK_IN_OUT(Double2DoubleFunction { x: Double ->
val c1 = 1.70158
val c2 = c1 * 1.525
if (x < 0.5) (2 * x).pow(2.0) * ((c2 + 1) * 2 * x - c2) / 2 else ((2 * x - 2).pow(2.0) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2
}),
BOUNCE_OUT(Double2DoubleFunction { x: Double ->
var x = x
val n1 = 7.5625
val d1 = 2.75
if (x < 1 / d1) {
return@Double2DoubleFunction n1 * x * x
} else if (x < 2 / d1) {
return@Double2DoubleFunction n1 * ((1.5 / d1).let { x -= it; x }) * x + 0.75
} else if (x < 2.5 / d1) {
return@Double2DoubleFunction n1 * ((2.25 / d1).let { x -= it; x }) * x + 0.9375
} else {
return@Double2DoubleFunction n1 * ((2.625 / d1).let { x -= it; x }) * x + 0.984375
}
}),
BOUNCE_IN(Double2DoubleFunction { x: Double -> (1 - BOUNCE_OUT.apply(x)).toDouble() }),
BOUNCE_IN_OUT(Double2DoubleFunction { x: Double ->
if (x < 0.5) ((1 - BOUNCE_OUT.apply(1 - 2 * x)) / 2).toDouble() else ((1 + BOUNCE_OUT.apply(
2 * x - 1
)) / 2).toDouble()
});
fun apply(f: Double): Float {
return floatFunction[f].toFloat()
}
}
}
@@ -0,0 +1,13 @@
package gg.norisk.heroes.common.networking.dto
import gg.norisk.heroes.common.serialization.BlockPosSerializer
import gg.norisk.heroes.common.serialization.BlockStateSerializer
import kotlinx.serialization.Serializable
import net.minecraft.block.BlockState
import net.minecraft.util.math.BlockPos
@Serializable
data class BlockInfoSmall(
@Serializable(with = BlockStateSerializer::class) val state: BlockState,
@Serializable(with = BlockPosSerializer::class) val pos: BlockPos
)
@@ -0,0 +1,10 @@
package gg.norisk.heroes.common.networking.dto
import kotlinx.serialization.Serializable
@Serializable
data class HeroSelectorPacket(
val heroes: List<String>,
val isActive: Boolean,
var isKitEditorEnabled: Boolean
)
@@ -0,0 +1,30 @@
package gg.norisk.heroes.common.networking.dto
import kotlinx.serialization.Serializable
enum class MouseType {
LEFT, MIDDLE, RIGHT
}
enum class MouseAction {
CLICK, RELEASE, HOLD
}
@Serializable
data class MousePacket(val type: MouseType, val action: MouseAction) {
fun isLeft(): Boolean = type == MouseType.LEFT
fun isRight(): Boolean = type == MouseType.RIGHT
fun isMiddle(): Boolean = type == MouseType.MIDDLE
fun isHolding(): Boolean = action == MouseAction.HOLD
fun isReleased(): Boolean = action == MouseAction.RELEASE
fun isClicked(): Boolean = action == MouseAction.CLICK
fun isHoldingLeftClick(): Boolean = isLeft() && isHolding()
fun isHoldingRightClick(): Boolean = isRight() && isHolding()
fun isHoldingMiddleClick(): Boolean = isMiddle() && isHolding()
override fun toString(): String {
return "[$type, $action]"
}
}
@@ -0,0 +1,70 @@
package gg.norisk.heroes.common.player
import gg.norisk.datatracker.entity.getSyncedData
import gg.norisk.datatracker.entity.setSyncedData
import gg.norisk.heroes.common.HeroesManager.logger
import gg.norisk.heroes.common.ability.PropertyPlayer
import gg.norisk.heroes.common.serialization.ItemStackSerializer
import gg.norisk.heroes.common.serialization.UUIDSerializer
import gg.norisk.heroes.common.utils.PlayStyle
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
import java.util.*
@Serializable
data class FFAPlayer(
@SerialName("_id")
@Serializable(with = UUIDSerializer::class)
val uuid: UUID,
var xp: Int = 0,
var kills: Int = 0,
var deaths: Int = 0,
var currentKillStreak: Int = 0,
var highestKillStreak: Int = 0,
var bounty: Int = 0,
var heroes: MutableMap<String, MutableMap<String, MutableMap<String, PropertyPlayer>>> = mutableMapOf(),
var inventorySorting: InventorySorting? = null,
)
@Serializable
data class InventorySorting(
@Serializable(with = UUIDSerializer::class)
val userId: UUID,
val playStyle: PlayStyle,
val version: Int,
val armor: Array<@Serializable(with = ItemStackSerializer::class) ItemStack> = Array(4) { ItemStack.EMPTY },
val offhand: Array<@Serializable(with = ItemStackSerializer::class) ItemStack> = Array(1) { ItemStack.EMPTY },
val main: Array<@Serializable(with = ItemStackSerializer::class) ItemStack> = Array(36) { ItemStack.EMPTY },
) {
companion object {
var CURRENT_VERSION = 0
fun ServerPlayerEntity.loadInventory(inventorySorting: InventorySorting) {
logger.info("Loading inventory $inventorySorting")
inventory.clear()
inventorySorting.armor.forEachIndexed { index, itemStack ->
inventory.armor[index] = itemStack.copy()
}
inventorySorting.main.forEachIndexed { index, itemStack ->
inventory.main[index] = itemStack.copy()
}
inventorySorting.offhand.forEachIndexed { index, itemStack ->
inventory.offHand[index] = itemStack.copy()
}
}
}
}
private const val FFA_PLAYER = "HeroApi:FfaPlayer"
var PlayerEntity.ffaPlayer: FFAPlayer
get() = this.getSyncedData<FFAPlayer>(FFA_PLAYER) ?: FFAPlayer(this.uuid)
set(value) = this.setSyncedData(FFA_PLAYER, value, (this as? ServerPlayerEntity?))
private const val FFA_BOUNTY = "HeroApi:Bounty"
var PlayerEntity.ffaBounty: Int
get() = this.getSyncedData<Int>(FFA_BOUNTY) ?: 0
set(value) = this.setSyncedData(FFA_BOUNTY, value)
@@ -0,0 +1,15 @@
package gg.norisk.heroes.common.registry
import gg.norisk.heroes.common.HeroesManager.toId
import net.minecraft.registry.Registries
import net.minecraft.registry.Registry
import net.minecraft.sound.SoundEvent
object SoundRegistry {
var FLYING = "flying".register()
fun init() {
}
private fun String.register() = Registry.register(Registries.SOUND_EVENT, this.toId(), SoundEvent.of(this.toId()))
}
@@ -0,0 +1,43 @@
package gg.norisk.heroes.common.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.*
import net.minecraft.util.math.BlockPos
object BlockPosSerializer : KSerializer<BlockPos> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockPos") {
element<Int>("x")
element<Int>("y")
element<Int>("z")
}
override fun serialize(encoder: Encoder, value: BlockPos) {
encoder.encodeStructure(descriptor) {
encodeIntElement(descriptor, 0, value.x)
encodeIntElement(descriptor, 1, value.y)
encodeIntElement(descriptor, 2, value.z)
}
}
override fun deserialize(decoder: Decoder): BlockPos {
return decoder.decodeStructure(descriptor) {
var x = 0
var y = 0
var z = 0
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> x = decodeIntElement(descriptor, 0)
1 -> y = decodeIntElement(descriptor, 1)
2 -> z = decodeIntElement(descriptor, 2)
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $index")
}
}
BlockPos(x, y, z)
}
}
}
@@ -0,0 +1,27 @@
package gg.norisk.heroes.common.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.block.BlockState
import net.minecraft.nbt.NbtHelper
import net.minecraft.nbt.StringNbtReader
import net.minecraft.registry.Registries
object BlockStateSerializer : KSerializer<BlockState> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockState")
override fun serialize(encoder: Encoder, value: BlockState) {
val nbt = NbtHelper.fromBlockState(value)
encoder.encodeString(nbt.asString())
}
override fun deserialize(decoder: Decoder): BlockState {
val nbtString = decoder.decodeString()
val nbt = StringNbtReader.parse(nbtString)
val blockState = NbtHelper.toBlockState(Registries.BLOCK.tagCreatingWrapper, nbt)
return blockState
}
}
@@ -0,0 +1,44 @@
package gg.norisk.heroes.common.serialization
import gg.norisk.heroes.common.HeroesManager.isServer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.fabricmc.api.EnvType
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.MinecraftClient
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtHelper
import net.silkmc.silk.core.Silk.serverOrThrow
object ItemStackSerializer : KSerializer<ItemStack> {
private val emptyItemStack = "EMPTY"
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ItemStack", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: ItemStack) {
if (value.isEmpty) {
encoder.encodeString(emptyItemStack)
return
}
val registryManager =
if (isServer) serverOrThrow.registryManager else MinecraftClient.getInstance().world?.registryManager
val nbt = value.encode(registryManager) as NbtCompound
val string = NbtHelper.toNbtProviderString(nbt)
encoder.encodeString(string)
}
override fun deserialize(decoder: Decoder): ItemStack {
val registryManager =
if (isServer) serverOrThrow.registryManager else MinecraftClient.getInstance().world?.registryManager
val string = decoder.decodeString()
if (string == emptyItemStack) {
return ItemStack.EMPTY
}
val nbt = NbtHelper.fromNbtProviderString(string)
return ItemStack.fromNbt(registryManager, nbt).orElseThrow()
}
}
@@ -0,0 +1,43 @@
package gg.norisk.heroes.common.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@PublishedApi
internal object StringSerializer : KSerializer<String> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun serialize(encoder: Encoder, value: String): Unit = String.serializer().serialize(encoder, value)
override fun deserialize(decoder: Decoder): String = String.serializer().deserialize(decoder)
}
@PublishedApi
internal object BooleanSerializer : KSerializer<Boolean> {
override val descriptor: SerialDescriptor = Boolean.serializer().descriptor
override fun serialize(encoder: Encoder, value: Boolean): Unit = Boolean.serializer().serialize(encoder, value)
override fun deserialize(decoder: Decoder): Boolean = Boolean.serializer().deserialize(decoder)
}
@PublishedApi
internal object IntSerializer : KSerializer<Int> {
override val descriptor: SerialDescriptor = Int.serializer().descriptor
override fun serialize(encoder: Encoder, value: Int): Unit = Int.serializer().serialize(encoder, value)
override fun deserialize(decoder: Decoder): Int = Int.serializer().deserialize(decoder)
}
@PublishedApi
internal object FloatSerializer : KSerializer<Float> {
override val descriptor: SerialDescriptor = Float.serializer().descriptor
override fun serialize(encoder: Encoder, value: Float): Unit = Float.serializer().serialize(encoder, value)
override fun deserialize(decoder: Decoder): Float = Float.serializer().deserialize(decoder)
}
@PublishedApi
internal object DoubleSerializer : KSerializer<Double> {
override val descriptor: SerialDescriptor = Double.serializer().descriptor
override fun serialize(encoder: Encoder, value: Double): Unit = Double.serializer().serialize(encoder, value)
override fun deserialize(decoder: Decoder): Double = Double.serializer().deserialize(decoder)
}
@@ -0,0 +1,113 @@
package gg.norisk.heroes.common.serialization
import com.mongodb.internal.connection.BsonWriterDecorator
import kotlinx.serialization.ExperimentalSerializationApi
import org.bson.AbstractBsonReader
import org.bson.AbstractBsonWriter
import org.bson.codecs.kotlinx.BsonDecoder
import org.bson.codecs.kotlinx.BsonEncoder
import java.lang.reflect.Field
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
@OptIn(ExperimentalSerializationApi::class)
object SerializationHelper {
fun isNameState(bsonEncoder: BsonEncoder): Boolean {
return WriterHelper.getState(bsonEncoder) == AbstractBsonWriter.State.NAME
}
fun isNameState(bsonDecoder: BsonDecoder): Boolean {
return ReaderHelper.isNameState(bsonDecoder)
}
private object WriterHelper {
private val BsonEncoder_writer_field: Field by lazy {
Class.forName("org.bson.codecs.kotlinx.JsonBsonEncoder").kotlin.memberProperties
.find { it.name == "writer" }?.javaField!!.also {
it.isAccessible = true
}
}
private val BsonWriterDecorator_bsonWriter_field: Field by lazy {
BsonWriterDecorator::class.memberProperties.find { it.name == "bsonWriter" }?.javaField!!.also {
it.isAccessible = true
}
}
private val BsonWriterDecorator_state_field: Field by lazy {
AbstractBsonWriter::class.memberProperties.find { it.name == "state" }?.javaField!!.also {
it.isAccessible = true
}
}
private fun `get BsonWriterDecorator of JsonBsonEncoder`(encoder: BsonEncoder): BsonWriterDecorator {
val writerDecorator = BsonEncoder_writer_field.get(encoder) as? BsonWriterDecorator
?: throw IllegalStateException("writerDecorator is not a ${BsonWriterDecorator::class.qualifiedName}.")
return writerDecorator
}
private fun `get AbstractBsonWriter of BsonWriterDecorator`(bsonWriterDecorator: BsonWriterDecorator): AbstractBsonWriter {
val abstractBsonWriter =
BsonWriterDecorator_bsonWriter_field.get(bsonWriterDecorator) as? AbstractBsonWriter
?: throw IllegalStateException("abstractBsonWriter is not a ${AbstractBsonWriter::class.qualifiedName}.")
return abstractBsonWriter
}
private fun `get State of AbstractBsonWriter`(abstractBsonWriter: AbstractBsonWriter): AbstractBsonWriter.State {
val state = BsonWriterDecorator_state_field.get(abstractBsonWriter) as? AbstractBsonWriter.State
?: throw IllegalStateException("abstractBsonWriter is not a ${AbstractBsonWriter::class.qualifiedName}.")
return state
}
fun getState(bsonEncoder: BsonEncoder): AbstractBsonWriter.State {
val bsonWriterDecoder = `get BsonWriterDecorator of JsonBsonEncoder`(bsonEncoder)
val abstractBsonWriter = `get AbstractBsonWriter of BsonWriterDecorator`(bsonWriterDecoder)
val state = `get State of AbstractBsonWriter`(abstractBsonWriter)
return state
}
}
private object ReaderHelper {
private val JsonBsonMapDecoderClass: Class<*> by lazy {
Class.forName("org.bson.codecs.kotlinx.JsonBsonMapDecoder")
}
/* private val AbstractBsonDecoderClass: Class<*> by lazy {
Class.forName("org.bson.codecs.kotlinx.AbstractBsonDecoder")
}*/
private val JsonBsonDocumentDecoder_reader_field: Field by lazy {
JsonBsonMapDecoderClass.kotlin.memberProperties.find { it.name == "reader" }?.javaField!!.also {
it.isAccessible = true
}
}
private fun `BsonDecoder is JsonBsonMapDecoder`(bsonDecoder: BsonDecoder): Boolean {
return JsonBsonMapDecoderClass.isInstance(bsonDecoder)
}
private fun `BsonDecoder as JsonBsonDocumentDecoder`(bsonDecoder: BsonDecoder): Any {
val jsonBsonDocumentDecoder = JsonBsonMapDecoderClass.cast(bsonDecoder)
return jsonBsonDocumentDecoder
}
private fun `get AbstractBsonReader of JsonBsonDocumentDecoder`(jsonBsonDocumentDecoder: Any): AbstractBsonReader {
val reader = JsonBsonDocumentDecoder_reader_field.get(jsonBsonDocumentDecoder) as AbstractBsonReader
return reader
}
private fun `get State of AbstractBsonReader`(abstractBsonReader: AbstractBsonReader): AbstractBsonReader.State {
return abstractBsonReader.state
}
private fun getState(bsonDecoder: BsonDecoder): AbstractBsonReader.State {
val jsonBsonDocumentDecoder = `BsonDecoder as JsonBsonDocumentDecoder`(bsonDecoder)
val abstractBsonReader = `get AbstractBsonReader of JsonBsonDocumentDecoder`(jsonBsonDocumentDecoder)
val state = `get State of AbstractBsonReader`(abstractBsonReader)
return state
}
fun isNameState(bsonDecoder: BsonDecoder): Boolean {
if (!`BsonDecoder is JsonBsonMapDecoder`(bsonDecoder)) return false
return getState(bsonDecoder) == AbstractBsonReader.State.NAME
}
}
}

Some files were not shown because too many files have changed in this diff Show More