public class DifficultyAffectedCapability implements IDifficultyAffected, ICapabilitySerializable<CompoundNBT> {
    @CapabilityInject(IDifficultyAffected.class)
    public static Capability<IDifficultyAffected> INSTANCE = null;
    public static ResourceLocation NAME = ScalingHealth.getId("difficulty_affected");

    private static final String NBT_BLIGHT = "Blight";
    private static final String NBT_DIFFICULTY = "Difficulty";

    private final LazyOptional<IDifficultyAffected> holder = LazyOptional.of(() -> this);

    private float difficulty;
    private boolean blight;
    private boolean processed;

    @Override
    public float getDifficulty() {
        return difficulty;
    }

    @Override
    public void setDifficulty(MobEntity mob) {
        difficulty = (float) ((Math.random()*(0.1)+0.95) * SHDifficulty.areaDifficulty(mob.world, mob.getPosition()));
    }

    @Override
    public void forceDifficulty(float diff) {
        difficulty = diff;
    }

    @Override
    public boolean isBlight() {
        return blight;
    }

    @Override
    public void setIsBlight(boolean value) {
        blight = value;
    }

    @Override
    public void setProcessed(boolean value) {
        this.processed = value;
    }

    @Override
    public void tick(MobEntity entity) {
        if (!processed && entity.isAlive() && entity.ticksExisted > 2) {
            setDifficulty(entity);
            MobDifficultyHandler.process(entity, this);
            processed = true;
            if (ScalingHealth.LOGGER.isDebugEnabled() && Config.COMMON.debugLogEntitySpawns.get())
                ScalingHealth.LOGGER.debug(DifficultyEvents.MARKER, "Processed {} -> difficulty={}, isBlight={}", entity, difficulty, blight);
        }
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
        return INSTANCE.orEmpty(cap, holder);
    }

    @Override
    public CompoundNBT serializeNBT() {
        CompoundNBT nbt = new CompoundNBT();
        nbt.putBoolean(NBT_BLIGHT, blight);
        nbt.putFloat(NBT_DIFFICULTY, difficulty);
        return nbt;
    }

    @Override
    public void deserializeNBT(CompoundNBT nbt) {
        blight = nbt.getBoolean(NBT_BLIGHT);
        difficulty = nbt.getFloat(NBT_DIFFICULTY);
    }

    public static boolean canAttachTo(ICapabilityProvider entity) {
        return entity instanceof MobEntity
                && !entity.getCapability(INSTANCE).isPresent()
                && SHMobs.allowsDifficultyChanges((MobEntity) entity);
    }

    public static void register() {
        CapabilityManager.INSTANCE.register(IDifficultyAffected.class, new Storage(), DifficultyAffectedCapability::new);
    }

    private static class Storage implements Capability.IStorage<IDifficultyAffected> {
        @Nullable
        @Override
        public INBT writeNBT(Capability<IDifficultyAffected> capability, IDifficultyAffected instance, Direction side) {
            if (instance instanceof DifficultyAffectedCapability) {
                return ((DifficultyAffectedCapability) instance).serializeNBT();
            }
            return new CompoundNBT();
        }

        @Override
        public void readNBT(Capability<IDifficultyAffected> capability, IDifficultyAffected instance, Direction side, INBT nbt) {
            if (instance instanceof DifficultyAffectedCapability) {
                ((DifficultyAffectedCapability) instance).deserializeNBT((CompoundNBT) nbt);
            }
        }
    }
} public class DifficultySourceCapability implements IDifficultySource, ICapabilitySerializable<CompoundNBT> {
    @CapabilityInject(IDifficultySource.class)
    public static Capability<IDifficultySource> INSTANCE = null;

    public static ResourceLocation NAME = ScalingHealth.getId("difficulty_source");
    private static IDifficultySource overworldCap = null;

    private static final String NBT_DIFFICULTY = "Difficulty";

    private final LazyOptional<IDifficultySource> holder = LazyOptional.of(() -> this);

    private float difficulty;
    private boolean exempt = false;

    public static Optional<IDifficultySource> getOverworldCap(){
        if(overworldCap == null) return Optional.empty();
        return Optional.of(overworldCap);
    }

    public static void setOverworldCap(IDifficultySource source){
        overworldCap = source;
    }

    @Override
    public float getDifficulty() {
        return difficulty;
    }

    @Override
    public void setDifficulty(float value) {
        if(exempt)
            difficulty = 0;
        else
            difficulty = value;
    }

    @Override
    public void setExempt(boolean exempt){
        this.exempt = exempt;
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
        return INSTANCE.orEmpty(cap, holder);
    }

    @Override
    public CompoundNBT serializeNBT() {
        CompoundNBT nbt = new CompoundNBT();
        nbt.putFloat(NBT_DIFFICULTY, difficulty);
        return nbt;
    }

    @Override
    public void deserializeNBT(CompoundNBT nbt) {
        difficulty = nbt.getFloat(NBT_DIFFICULTY);
    }

    public static boolean canAttachTo(ICapabilityProvider obj) {
        try {
            if (obj.getCapability(INSTANCE).isPresent()) {
                return false;
            }
        } catch (NullPointerException ex) {
            ScalingHealth.LOGGER.error("Failed to get capabilities from {}", obj);
            return false;
        }
        return obj instanceof PlayerEntity || (obj instanceof ServerWorld && ((ServerWorld) obj).dimension.getType() == DimensionType.OVERWORLD);
    }

    public static void register() {
        CapabilityManager.INSTANCE.register(IDifficultySource.class, new Storage(), DifficultySourceCapability::new);
    }

    private static class Storage implements Capability.IStorage<IDifficultySource> {
        @Nullable
        @Override
        public INBT writeNBT(Capability<IDifficultySource> capability, IDifficultySource instance, Direction side) {
            if (instance instanceof DifficultySourceCapability) {
                return ((DifficultySourceCapability) instance).serializeNBT();
            }
            return new CompoundNBT();
        }

        @Override
        public void readNBT(Capability<IDifficultySource> capability, IDifficultySource instance, Direction side, INBT nbt) {
            if (instance instanceof DifficultySourceCapability) {
                ((DifficultySourceCapability) instance).deserializeNBT((CompoundNBT) nbt);
            }
        }
    }
} public class PetHealthCapability implements IPetData, ICapabilitySerializable<CompoundNBT> {
    @CapabilityInject(IPetData.class)
    public static Capability<IPetData> INSTANCE = null;
    public static ResourceLocation NAME = ScalingHealth.getId("pet_health");

    private static final String NBT_HEALTH = "SHPetBonusHealth";

    private final LazyOptional<IPetData> holder = LazyOptional.of(() -> this);

    private float bonusHealth;
    private boolean refreshed = false;

    @Override
    public void addHealth(double hp, TameableEntity pet ) {
        bonusHealth += hp;
        ModifierHandler.setMaxHealth(pet, bonusHealth, AttributeModifier.Operation.ADDITION);
    }

    @Override
    public float getBonusHealth() {
        return bonusHealth;
    }

    @Override
    public void tick(TameableEntity pet) {
        if(!refreshed && pet.ticksExisted > 2){
            refreshed = true;
            ModifierHandler.setMaxHealth(pet, getBonusHealth(), AttributeModifier.Operation.ADDITION);
        }
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
        return INSTANCE.orEmpty(cap, holder);
    }

    @Override
    public CompoundNBT serializeNBT() {
        CompoundNBT nbt = new CompoundNBT();
        nbt.putFloat(NBT_HEALTH, bonusHealth);
        return nbt;
    }

    @Override
    public void deserializeNBT(CompoundNBT nbt) {
        bonusHealth = nbt.getFloat(NBT_HEALTH);
    }

    public static boolean canAttachTo(ICapabilityProvider obj) {
        try {
            if (obj.getCapability(INSTANCE).isPresent()) {
                return false;
            }
        } catch (NullPointerException ex) {
            ScalingHealth.LOGGER.error("Failed to get capabilities from {}", obj);
            return false;
        }
        return obj instanceof TameableEntity;
    }

    public static void register() {
        CapabilityManager.INSTANCE.register(IPetData.class, new Storage(), PetHealthCapability::new);
    }

    private static class Storage implements Capability.IStorage<IPetData> {
        @Nullable
        @Override
        public INBT writeNBT(Capability<IPetData> capability, IPetData instance, Direction side) {
            if (instance instanceof PetHealthCapability) {
                return ((PetHealthCapability) instance).serializeNBT();
            }
            return new CompoundNBT();
        }

        @Override
        public void readNBT(Capability<IPetData> capability, IPetData instance, Direction side, INBT nbt) {
            if (instance instanceof PetHealthCapability) {
                ((PetHealthCapability) instance).deserializeNBT((CompoundNBT) nbt);
            }
        }
    }
} public class PlayerDataCapability implements IPlayerData, ICapabilitySerializable<CompoundNBT> {
    @CapabilityInject(IPlayerData.class)
    public static Capability<IPlayerData> INSTANCE = null;
    public static ResourceLocation NAME = ScalingHealth.getId("player_data");

    private static final String NBT_HEART_CRYSTALS = "HeartCrystals";
    private static final String NBT_POWER_CRYSTALS = "PowerCrystals";

    private final LazyOptional<IPlayerData> holder = LazyOptional.of(() -> this);

    private boolean afk = false;
    private int timeAfk = 0;
    private BlockPos lastPos;
    private int heartByXp;
    private int heartByCrystals;
    private int powerCrystals;

    @Override
    public int getAllHearts() {
        return heartByXp + heartByCrystals;
    }

    @Override
    public int getHeartByCrystals() {
        return heartByCrystals;
    }

    @Override
    public void setXpHearts(PlayerEntity player, int amount) {
        heartByXp = SHPlayers.clampExtraHearts(amount);
        ModifierHandler.setMaxHealth(player, getHealthModifier(), AttributeModifier.Operation.ADDITION);
    }

    @Override
    public void setHeartByCrystals(PlayerEntity player, int amount) {
        heartByCrystals = SHPlayers.clampExtraHearts(amount);
        ModifierHandler.setMaxHealth(player, getHealthModifier(), AttributeModifier.Operation.ADDITION);
    }

    @Override
    public int getPowerCrystals() {
        return powerCrystals;
    }

    @Override
    public void setPowerCrystalCount(PlayerEntity player, int amount) {
        powerCrystals = SHPlayers.clampPowerCrystals(amount);
        ModifierHandler.addAttackDamage(player, getAttackDamageModifier(), AttributeModifier.Operation.ADDITION);
    }

    @Override
    public void updateStats(PlayerEntity player) {
        ModifierHandler.setMaxHealth(player, getHealthModifier(), AttributeModifier.Operation.ADDITION);
        ModifierHandler.addAttackDamage(player, getAttackDamageModifier(), AttributeModifier.Operation.ADDITION);
    }

    @Override
    public void tick(PlayerEntity player) {
        if(player.world.getGameTime() % 20 == 0 && !player.world.isRemote){
            checkPlayerIdle(player);

            if(player instanceof ServerPlayerEntity)
                IPlayerData.sendUpdatePacketTo(player);
        }
    }

    private void checkPlayerIdle(PlayerEntity player){
        if(SHDifficulty.areaDifficulty(player.world, player.getPosition()) >= SHDifficulty.maxValue()) return;

        if(player.getPosition().equals(lastPos)){
            timeAfk++;
        }
        else {
            afk = false;
            timeAfk = 0;
        }

        lastPos = player.getPosition();
        if(timeAfk > SHDifficulty.timeBeforeAfk()){
            if(!afk) {
                afk = true;
                if(SHDifficulty.afkMessage()) player.sendMessage(new TranslationTextComponent("misc.scalinghealth.afkmessage"));
            }
        }

        if(afk) {
            IDifficultySource data = SHDifficulty.source(player);
            float changePerSec = (float) SHDifficulty.changePerSecond();
            //since last second we added "changePerSec" difficulty, we subtract an amount based on idlemodifier
            data.addDifficulty(- changePerSec * (float) (1 - SHDifficulty.idleModifier()));
        }
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
        return INSTANCE.orEmpty(cap, holder);
    }

    @Override
    public CompoundNBT serializeNBT() {
        CompoundNBT nbt = new CompoundNBT();
        nbt.putInt(NBT_HEART_CRYSTALS, heartByCrystals);
        nbt.putInt(NBT_POWER_CRYSTALS, powerCrystals);
        return nbt;
    }

    @Override
    public void deserializeNBT(CompoundNBT nbt) {
        heartByCrystals = nbt.getInt(NBT_HEART_CRYSTALS);
        powerCrystals = nbt.getInt(NBT_POWER_CRYSTALS);
    }

    public static boolean canAttachTo(ICapabilityProvider entity) {
        if (!(entity instanceof PlayerEntity)) {
            return false;
        }
        try {
            if (entity.getCapability(INSTANCE).isPresent()) {
                return false;
            }
        } catch (NullPointerException ex) {
            // Forge seems to be screwing up somewhere?
            ScalingHealth.LOGGER.error("Failed to get capabilities from {}", entity);
            return false;
        }
        return true;
    }

    public static void register() {
        CapabilityManager.INSTANCE.register(IPlayerData.class, new Storage(), PlayerDataCapability::new);
    }

    private static class Storage implements Capability.IStorage<IPlayerData> {
        @Nullable
        @Override
        public INBT writeNBT(Capability<IPlayerData> capability, IPlayerData instance, Direction side) {
            if (instance instanceof PlayerDataCapability) {
                return ((PlayerDataCapability) instance).serializeNBT();
            }
            return new CompoundNBT();
        }

        @Override
        public void readNBT(Capability<IPlayerData> capability, IPlayerData instance, Direction side, INBT nbt) {
            if (instance instanceof PlayerDataCapability) {
                ((PlayerDataCapability) instance).deserializeNBT((CompoundNBT) nbt);
            }
        }
    }
} public class KeyManager {
    public static final KeyBinding toggleDiff = new KeyBinding("key.scalinghealth.difficulty", GLFW.GLFW_KEY_Z, ScalingHealth.MOD_NAME);

    public static void registerBindings(){
        ClientRegistry.registerKeyBinding(toggleDiff);
    }
} public class DebugOverlay extends DebugRenderOverlay {
    private static final String FLOAT_FORMAT = "%.5f";

    public static DebugOverlay INSTANCE = new DebugOverlay();

    public static void init() { }

    @Nonnull
    @Override
    public List<String> getDebugText() {
        ClientPlayerEntity player = Minecraft.getInstance().player;
        if (player == null) return ImmutableList.of();

        return ImmutableList.of(
                "Difficulty (/" + ClientHandler.maxDifficultyValue + ")",
                "- Mode=" + ClientHandler.areaMode.getDisplayName().getFormattedText(),
                "- Player=" + String.format(FLOAT_FORMAT, ClientHandler.playerDifficulty),
                "- Server=" + String.format(FLOAT_FORMAT, DifficultySourceCapability.getOverworldCap().orElseGet(DifficultySourceCapability::new).getDifficulty()),
                "- Area=" + String.format(FLOAT_FORMAT + " (x%.1f, ☽x%.1f)",
                        ClientHandler.areaDifficulty,
                        ClientHandler.locationMultiPercent / 100f,
                        SHDifficulty.lunarMultiplier(player.world)),
                "Health",
                "- Health=" + String.format("%.5f / %.1f", player.getHealth(), player.getMaxHealth()),
                "- Regen=" + String.format("%ds", ClientHandler.regenTimer / 20)
        );
    }

    @Override
    public float getTextScale() {
        return 0.75f;
    }

    @Override
    public boolean isHidden() {
        return !Config.COMMON.debugShowOverlay.get();
    }

    @Override
    public Anchor getAnchorPoint() {
        return Config.CLIENT.debugOverlayAnchor.get();
    }
} public class DifficultyMeter extends Screen {
    public static final DifficultyMeter INSTANCE = new DifficultyMeter(new StringTextComponent(""));

    private static final ResourceLocation TEXTURE = new ResourceLocation(ScalingHealth.MOD_ID, "textures/gui/hud.png");

    private int lastDifficultyDisplayed = -100;
    private int lastAreaDifficultyDisplayed = -100;
    private int lastUpdateTime = Integer.MIN_VALUE;

    private boolean keyDown = false;

    protected DifficultyMeter(ITextComponent title) {
        super(title);
    }

    @SubscribeEvent
    public void onTick(InputEvent.KeyInputEvent event){
        if(!EnabledFeatures.difficultyEnabled()) return;

        DifficultyMeterShow showMode = Config.CLIENT.difficultyMeterShow.get();
        if(event.getKey() != KeyManager.toggleDiff.getKey().getKeyCode()  || showMode != DifficultyMeterShow.KEYPRESS)
            return;
        if(event.getAction() == GLFW.GLFW_PRESS){
            keyDown = true;
        }
        if(event.getAction() == GLFW.GLFW_RELEASE){
            keyDown = false;
        }
    }

    @SubscribeEvent
    public void onRenderOverlay(RenderGameOverlayEvent.Post event) {
        if(!EnabledFeatures.difficultyEnabled()) return;

        DifficultyMeterShow showMode = Config.CLIENT.difficultyMeterShow.get();
        if (event.getType() != RenderGameOverlayEvent.ElementType.TEXT || showMode == DifficultyMeterShow.NEVER) {
            return;
        }

        Minecraft mc = Minecraft.getInstance();
        PlayerEntity player = mc.player;
        if (player == null) return;

        final double maxDifficulty = ClientHandler.maxDifficultyValue;
        if (maxDifficulty <= 0) return;

        int width = mc.mainWindow.getScaledWidth();
        int height = mc.mainWindow.getScaledHeight();

        AreaDifficultyMode areaMode = ClientHandler.areaMode;
        int preClampAreaDifficulty = (int) ClientHandler.areaDifficulty;
        int areaDifficulty = MathHelper.clamp(preClampAreaDifficulty, 0, (int) maxDifficulty);
        int difficulty = areaMode == AreaDifficultyMode.SERVER_WIDE
                ? areaDifficulty
                : (int) ClientHandler.playerDifficulty;
        int timeSinceLastUpdate = ClientTicks.ticksInGame() - lastUpdateTime;

        if (difficulty != lastDifficultyDisplayed) {
            lastDifficultyDisplayed = difficulty;
            lastUpdateTime = ClientTicks.ticksInGame();
        }
        if (areaDifficulty < lastAreaDifficultyDisplayed - 10 || areaDifficulty > lastAreaDifficultyDisplayed + 10 && timeSinceLastUpdate > 1200) {
            lastAreaDifficultyDisplayed = areaDifficulty;
            lastUpdateTime = ClientTicks.ticksInGame();
        }

        int currentTime = ClientTicks.ticksInGame();
        if (showMode == DifficultyMeterShow.ALWAYS || currentTime - lastUpdateTime < 20 * Config.CLIENT.difficultyMeterShowTime.get() || keyDown) {
            GlStateManager.enableBlend();

            mc.textureManager.bindTexture(TEXTURE);

            GlStateManager.pushMatrix();
            // GlStateManager.scale(1f, 0.5f, 1f);

            Anchor anchor = Config.CLIENT.difficultyMeterAnchor.get();
            int posX = anchor.getX(width, 66, 5) + Config.CLIENT.difficultyMeterOffsetX.get();
            int posY = anchor.getY(height, 14, 5) + Config.CLIENT.difficultyMeterOffsetY.get();

            // Frame
            blitWithColor(posX, posY, 190, 0, 66, 14, 0xFFFFFF);

            // Area Difficulty
            int barLength = (int) (60 * areaDifficulty / maxDifficulty);
            blitWithColor(posX + 3, posY + 5, 193, 19, barLength, 6, 0xFFFFFF);

            // Player Difficulty
            barLength = (int) (60 * difficulty / maxDifficulty);
            blitWithColor(posX + 3, posY + 3, 193, 17, barLength, 2, 0xFFFFFF);

            // Text
            final float textScale = Config.CLIENT.difficultyMeterTextScale.get().floatValue();
            if (textScale > 0) {
                GlStateManager.pushMatrix();
                GlStateManager.scalef(textScale, textScale, 1);
                mc.fontRenderer.drawStringWithShadow(
                        I18n.format("misc.scalinghealth.difficultyMeterText"),
                        posX / textScale + 4,
                        posY / textScale - 9,
                        0xFFFFFF);

                // Text Difficulty
                String str = String.format("%d", areaDifficulty);
                int strWidth = mc.fontRenderer.getStringWidth(str);
                mc.fontRenderer.drawStringWithShadow(str,
                        posX / textScale + 104 - strWidth,
                        posY / textScale - 9,
                        0xAAAAAA);
                GlStateManager.popMatrix();
            }

            GlStateManager.popMatrix();
            GlStateManager.disableBlend();
        }
    }

    private void blitWithColor(int x, int y, int textureX, int textureY, int width, int height, int color) {
        float r = ((color >> 16) & 255) / 255f;
        float g = ((color >> 8) & 255) / 255f;
        float b = (color & 255) / 255f;
        GlStateManager.color3f(r, g, b);
        blit(x, y, textureX, textureY, width, height);
        GlStateManager.color3f(1, 1, 1);
    }
} public final class HeartDisplayHandler extends Screen {
    public static final HeartDisplayHandler INSTANCE = new HeartDisplayHandler(new StringTextComponent(""));

    private static final float COLOR_CHANGE_PERIOD = 150;
    private static final ResourceLocation TEXTURE = new ResourceLocation(ScalingHealth.MOD_ID, "textures/gui/hud.png");

    private static final TextureSlice TANK_SHINE = new TextureSlice(44, 0, 5, 5);
    private static final TextureSlice TANK_OUTLINE = new TextureSlice(44, 5, 5, 5);
    private static final TextureSlice TANK_FULL = new TextureSlice(44, 10, 5, 5);
    private static final TextureSlice TANK_EMPTY = new TextureSlice(44, 15, 5, 5);

    private final HeartsInfo info = new HeartsInfo();

    private HeartDisplayHandler(ITextComponent title) {
        super(title);
    }

    @SubscribeEvent(receiveCanceled = true)
    public void onHealthBar(RenderGameOverlayEvent.Pre event) {
        if (info.heartStyle == HeartIconStyle.VANILLA) return;

        Minecraft mc = Minecraft.getInstance();
        PlayerEntity player = mc.player;

        // Health text
        if (event.getType() == RenderGameOverlayEvent.ElementType.TEXT && mc.playerController.gameIsSurvivalOrAdventure()) {
            // Draw health string?
            if (Config.CLIENT.healthTextStyle.get() != HealthTextStyle.DISABLED) {
                mc.getProfiler().startSection("scalinghealthRenderHealthText");
                renderHealthText(mc, info.health, info.maxHealth,
                        -91 + Config.CLIENT.healthTextOffsetX.get(),
                        -38 + Config.CLIENT.healthTextOffsetY.get(),
                        Config.CLIENT.healthTextStyle.get(),
                        Config.CLIENT.healthTextColorStyle.get());
                mc.getProfiler().endSection();
            }
            // Draw absorption amount string?
            if (Config.CLIENT.absorptionTextStyle.get() != HealthTextStyle.DISABLED && player.getAbsorptionAmount() > 0) {
                mc.getProfiler().startSection("scalinghealthRenderAbsorptionText");
                renderHealthText(mc, player.getAbsorptionAmount(), 0,
                        -91 + Config.CLIENT.absorptionTextOffsetX.get(),
                        -49 + Config.CLIENT.absorptionTextOffsetY.get(),
                        Config.CLIENT.absorptionTextStyle.get(),
                        HealthTextColor.SOLID);
                mc.getProfiler().endSection();
            }
        }

        // Hearts
        if (event.getType() == RenderGameOverlayEvent.ElementType.HEALTH && info.heartStyle != HeartIconStyle.VANILLA) {
            event.setCanceled(true);
            mc.getProfiler().startSection("scalinghealthRenderHearts");
            renderHearts(event, mc, player);
            mc.getProfiler().endSection();
        }
    }

    private void renderHearts(RenderGameOverlayEvent event, Minecraft mc, PlayerEntity player) {
        info.update();

        GlStateManager.enableBlend();

        float absorb = MathHelper.ceil(player.getAbsorptionAmount());

        final int left = info.scaledWindowWidth / 2 - 91;
        int top = info.scaledWindowHeight - ForgeIngameGui.left_height;
        ForgeIngameGui.left_height += info.rowsUsedInHud * info.rowHeight;
        if (info.rowHeight != 10)
            ForgeIngameGui.left_height += 10 - info.rowHeight;

        // Draw vanilla hearts
        drawVanillaHearts(left, top);

        int potionOffset = info.hardcoreMode ? 27 : 0;

        // Draw extra hearts (only top 2 rows)
        mc.textureManager.bindTexture(TEXTURE);
        int rowCount = info.getCustomHeartRowCount(info.healthInt);
        int maxHealthRows = info.getCustomHeartRowCount((int) player.getMaxHealth());

        for (int row = Math.max(0, rowCount - 2); row < rowCount; ++row) {
            int actualRow = info.getActualRow(row);
            int renderHearts = info.getHeartsInRows(actualRow);
            int rowColor = getColorForRow(row, false);

            // Draw the hearts
            int j;
            for (j = 0; j < renderHearts; ++j) {
                int y = info.offsetHeartPosY(j, top);
                blitWithColor(left + 8 * j, y, 0, potionOffset, 9, 9, rowColor);
            }
            boolean anythingDrawn = j > 0;

            // Half heart on the end?
            if (info.healthInt % 2 == 1 && renderHearts < 10) {
                int y = info.offsetHeartPosY(j, top);
                blitWithColor(left + 8 * renderHearts, y, 9, potionOffset, 9, 9, rowColor);
                anythingDrawn = true;
            }

            // Outline for last heart, to make seeing max health a little easier.
            if (Config.CLIENT.lastHeartOutline.get() && anythingDrawn && row == maxHealthRows - 1) {
                // Get position of last partial/full heart
                j = (int) (Math.ceil(player.getMaxHealth() % 20f / 2f)) - 1;
                if (j < 0) j += 10;
                int y = info.offsetHeartPosY(j, top);
                int color = Config.CLIENT.lastHeartOutlineColor.get();
                blitWithColor(left + 8 * j, y, 17, 9, 9, 9, color);
            }
        }

        for (int i = 0; i < 10 && i < Math.ceil(info.healthInt / 2f); ++i) {
            int y = info.offsetHeartPosY(i, top);
            // Effect hearts (poison, wither)
            if (showEffectHearts(player)) {
                int color = effectHeartColor(player);
                blitWithColor(left + 8 * i, y, 0, 54, 9, 9, color);
            }
            // Shiny glint on top of the hearts, a single white pixel in the upper left <3
            if (!info.hardcoreMode) {
                blitWithColor(left + 8 * i, y, 17, 0, 9, 9, 0xCCFFFFFF);
            }
        }

        // Tanks

        if (info.getMaxHeartTanks() > 0 && Config.CLIENT.heartTanks.get()) {
            int tankRows = info.getHeartTankRowCount();
            int maxTankRows = info.getMaxHeartTankRowCount();

            for (int row = 0; row < maxTankRows; ++row) {
                int filledTanksInRow = info.getFilledHeartTanksInRow(row);
                int allTanksInRow = info.getAllHeartTanksInRow(row);
                int rowColor = getColorForRow(row, false);
                top -= 4;
                ForgeIngameGui.left_height += 4;

                // Draw tanks
                int x;
                for (x = 0; x < allTanksInRow; ++x) {
                    if (x < filledTanksInRow) {
                        TANK_FULL.blit(left + 4 * x, top, rowColor, this);
                    } else {
                        TANK_EMPTY.blit(left + 4 * x, top, 0xFFFFFF, this);
                    }
                }
                boolean anythingDrawn = x > 0;

                if (Config.CLIENT.lastHeartOutline.get() && anythingDrawn && row == maxTankRows - 1) {
                    x = (int) (Math.ceil(allTanksInRow)) - 1;
                    if (x < 0) x += 20;
                    TANK_OUTLINE.blit(left + 4 * x, top, Config.CLIENT.lastHeartOutlineColor.get(), this);
                }
            }
        }

        // ==========================
        // Absorption hearts override
        // ==========================

        AbsorptionIconStyle absorptionIconStyle = Config.CLIENT.absorptionIconStyle.get();
        if (absorptionIconStyle != AbsorptionIconStyle.VANILLA) {
            int absorbCeil = (int) Math.ceil(absorb);
            rowCount = (int) Math.ceil(absorb / 20);

            // Dark underlay for first row
            int texX = 17;
            int texY = absorptionIconStyle == AbsorptionIconStyle.SHIELD ? 45 : 54;
            for (int i = 0; i < 10 && i < absorb / 2; ++i) {
                int y = info.offsetAbsorptionPosY(i, top);
                blitWithColor(left + 8 * i, y, texX, texY, 9, 9, 0xFFFFFF);
            }

            // Draw the top two absorption rows, just the basic "hearts"
            texX = absorptionIconStyle == AbsorptionIconStyle.SHIELD ? 26 : 0;
            texY = absorptionIconStyle == AbsorptionIconStyle.SHIELD ? 0 : potionOffset;
            for (int i = Math.max(0, rowCount - 2); i < rowCount; ++i) {
                int renderHearts = Math.min((absorbCeil - 20 * i) / 2, 10);
                int rowColor = getColorForRow(i, true);
                boolean anythingDrawn;

                // Draw the hearts
                int x;
                for (x = 0; x < renderHearts; ++x) {
                    int y = info.offsetAbsorptionPosY(x, top);
                    blitWithColor(left + 8 * x, y, texX, texY, 9, 9, rowColor);
                }
                anythingDrawn = x > 0;

                // Half heart on the end?
                if (absorbCeil % 2 == 1 && renderHearts < 10) {
                    int y = info.offsetAbsorptionPosY(x, top);
                    blitWithColor(left + 8 * renderHearts, y, texX + 9, texY, 9, 9, rowColor);
                    anythingDrawn = true;
                }
            }

            // Add extra bits like outlines on top
            for (int i = 0; i < 10 && i < absorb / 2; ++i) {
                int y = info.offsetAbsorptionPosY(i, top);
                if (absorptionIconStyle == AbsorptionIconStyle.SHIELD) {
                    // Golden hearts in center (shield style only)
                    blitWithColor(left + 8 * i, y, 17, 36, 9, 9, 0xFFFFFF);
                } else if (absorptionIconStyle == AbsorptionIconStyle.GOLD_OUTLINE) {
                    // Golden outline
                    blitWithColor(left + 8 * i, y, 17, 27, 9, 9, 0xFFFFFF);
                }
                // Shiny glint on top, same as hearts.
                if (!info.hardcoreMode || absorptionIconStyle == AbsorptionIconStyle.SHIELD) {
                    blitWithColor(left + 8 * i, y, 17, 0, 9, 9, 0xCCFFFFFF);
                }
            }
        }

        GlStateManager.disableBlend();
        mc.textureManager.bindTexture(Screen.GUI_ICONS_LOCATION);
    }

    private void drawVanillaHearts(int left, int top) {
        int textureX = info.recentlyHurtHighlight ? 25 : 16;
        int textureY = 9 * (info.hardcoreMode ? 5 : 0);
        int margin = 16;

        float healthMax = Math.min(info.maxHealth, 20);
        float absorbRemaining = info.absorption;
        float healthTotal = info.healthInt + info.absorptionInt;

        int iStart = MathHelper.ceil((healthMax + (info.absorptionStyle == AbsorptionIconStyle.VANILLA ? info.absorptionInt : 0)) / 2f) - 1;
        for (int i = iStart; i >= 0; --i) {
            int row = MathHelper.ceil((i + 1) / 10f) - 1;
            int x = left + i % 10 * 8;
            int y = info.offsetHeartPosY(i, top - row * info.rowHeight);

            blit(x, y, textureX, textureY, 9, 9);

            if (info.recentlyHurtHighlight) {
                if (i * 2 + 1 < info.previousHealthInt)
                    blit(x, y, margin + 54, textureY, 9, 9);
                else if (i * 2 + 1 == info.previousHealthInt)
                    blit(x, y, margin + 63, textureY, 9, 9);
            }

            if (absorbRemaining > 0f && info.absorptionStyle == AbsorptionIconStyle.VANILLA) {
                if (MathUtils.doublesEqual(absorbRemaining, info.absorption) && MathUtils.doublesEqual(info.absorption % 2f, 1f)) {
                    blit(x, y, margin + 153, textureY, 9, 9);
                    absorbRemaining -= 1f;
                } else {
                    if (i * 2 + 1 < healthTotal)
                        blit(x, y, margin + 144, textureY, 9, 9);
                    absorbRemaining -= 2f;
                }
            } else {
                if (i * 2 + 1 < info.healthInt)
                    blit(x, y, margin + 36, textureY, 9, 9);
                else if (i * 2 + 1 == info.healthInt)
                    blit(x, y, margin + 45, textureY, 9, 9);
            }
        }
    }

    private void renderHealthText(Minecraft mc, float current, float max, int offsetX, int offsetY, HealthTextStyle style, HealthTextColor styleColor) {
        final double scale = style.getScale();
        final int left = (int) ((info.scaledWindowWidth / 2 + offsetX) / scale);
        // GuiIngameForge.left_height == 59 in normal cases. Making it a constant should fix some issues.
        final int top = (int) ((info.scaledWindowHeight + offsetY + (1 / scale)) / scale);

        // Draw health string
        mc.getProfiler().startSection("shTextPreDraw");
        String healthString = style.textFor(current, max);
        FontRenderer fontRenderer = Minecraft.getInstance().fontRenderer;
        int stringWidth = fontRenderer.getStringWidth(healthString);
        int color;
        float divisor = max == 0 ? current : max;
        switch (styleColor) {
            case TRANSITION:
//                color = Color.HSBtoRGB(0.34f * current / divisor, 0.7f, 1.0f);
                color = Color.blend(
                        Config.CLIENT.healthTextEmptyColor.get(),
                        Config.CLIENT.healthTextFullColor.get(),
                        current / divisor);
                break;
            case PSYCHEDELIC:
                color = java.awt.Color.HSBtoRGB(
                        (ClientTicks.ticksInGame() % COLOR_CHANGE_PERIOD) / COLOR_CHANGE_PERIOD,
                        0.55f * current / divisor, 1.0f);
                break;
            case SOLID:
            default:
                color = Config.CLIENT.healthTextFullColor.get();
                break;
        }
        mc.getProfiler().endSection();

        mc.getProfiler().startSection("shTextDraw");
        GlStateManager.pushMatrix();
        GlStateManager.scaled(scale, scale, 1);
        fontRenderer.drawStringWithShadow(healthString, left - stringWidth - 2, top, color);
        GlStateManager.popMatrix();
        mc.getProfiler().endSection();
    }

    private void blitWithColor(int x, int y, int textureX, int textureY, int width, int height, int color) {
        float a = ((color >> 24) & 255) / 255f;
        if (a <= 0f)
            a = 1f;
        float r = ((color >> 16) & 255) / 255f;
        float g = ((color >> 8) & 255) / 255f;
        float b = (color & 255) / 255f;
        GlStateManager.color4f(r, g, b, a);
        blit(x, y, textureX, textureY, width, height);
        GlStateManager.color4f(1, 1, 1, 1);
    }

    private static int getColorForRow(int row, boolean absorption) {
        List<Integer> colors = absorption ? Config.CLIENT.absorptionHeartColors.get() : Config.CLIENT.heartColors.get();
        int index = Config.CLIENT.heartColorLooping.get()
                ? row % colors.size()
                : MathUtils.clamp(row, 0, colors.size() - 1);
        return colors.get(index);
    }

    private static boolean showEffectHearts(PlayerEntity player) {
        return player.isPotionActive(Effects.POISON) || player.isPotionActive(Effects.WITHER);
    }

    private static int effectHeartColor(PlayerEntity player) {
        if (player.isPotionActive(Effects.WITHER))
            return 0x663E47;
        if (player.isPotionActive(Effects.POISON))
            return 0x4E9331;
        return 0xFFFFFF;
    }
} public final class ModCommands {
    private ModCommands() {throw new IllegalAccessError("Utility class");}

    public static void registerAll(CommandDispatcher<CommandSource> dispatcher) {
        DifficultyCommand.register(dispatcher);
        HealthCommand.register(dispatcher);
        PowerCommand.register(dispatcher);
        RecalculateCommand.register(dispatcher);
        SummonCommand.register(dispatcher);
    }

    static ITextComponent playerNameText(PlayerEntity player) {
        return new TranslationTextComponent("command.scalinghealth.playerName",
                player.getName().applyTextStyle(TextFormatting.ITALIC)).applyTextStyle(TextFormatting.AQUA);
    }

    static ITextComponent valueText(double value, double maxValue) {
        return new TranslationTextComponent("command.scalinghealth.valueOverMax",
                String.format("%.1f", value),
                String.format("%.1f", maxValue)
        ).applyTextStyle(TextFormatting.WHITE);
    }
} public final class Config {
    // config/scaling-health
    private static final Path SH_CONFIG_DIR = resolve(FMLPaths.CONFIGDIR.get(), "scaling-health");

    private static final ConfigSpecWrapper WRAPPER_GAME = ConfigSpecWrapper.create(resolve(SH_CONFIG_DIR, "game_settings.toml"));
    private static final ConfigSpecWrapper WRAPPER_CLIENT = ConfigSpecWrapper.create(resolve(SH_CONFIG_DIR, "client.toml"));
    private static final ConfigSpecWrapper WRAPPER_COMMON = ConfigSpecWrapper.create(resolve(SH_CONFIG_DIR, "common.toml"));

    public static final Client CLIENT = new Client(WRAPPER_CLIENT);
    public static final Common COMMON = new Common(WRAPPER_COMMON);
    public static final GameConfig GENERAL = new GameConfig(WRAPPER_GAME);

    public static class Client {
        // Debug
        public final EnumValue<Anchor> debugOverlayAnchor;
        public final DoubleValue debugOverlayTextScale;

        // Health Icons
        public final EnumValue<HeartIconStyle> heartIconStyle;
        public final ColorList heartColors;
        public final BooleanValue lastHeartOutline;
        public final IntValue lastHeartOutlineColor;
        public final BooleanValue heartColorLooping;
        // Heart Tanks
        public final BooleanValue heartTanks;

        // Health Text
        public final EnumValue<HealthTextStyle> healthTextStyle;
        public final DoubleValue healthTextScale;
        public final IntValue healthTextOffsetX;
        public final IntValue healthTextOffsetY;
        public final EnumValue<HealthTextColor> healthTextColorStyle;
        public final IntValue healthTextFullColor;
        public final IntValue healthTextEmptyColor;

        // Absorption Icons
        public final EnumValue<AbsorptionIconStyle> absorptionIconStyle;
        public final ColorList absorptionHeartColors;

        // Absorption Text
        public final EnumValue<HealthTextStyle> absorptionTextStyle;
        public final IntValue absorptionTextOffsetX;
        public final IntValue absorptionTextOffsetY;
        public final IntValue absorptionTextColor;

        // Difficulty
        public final BooleanValue warnWhenSleeping;
        // Difficulty Meter
        public final EnumValue<DifficultyMeterShow> difficultyMeterShow;
        public final DoubleValue difficultyMeterShowTime;
        public final EnumValue<Anchor> difficultyMeterAnchor;
        public final IntValue difficultyMeterOffsetX;
        public final IntValue difficultyMeterOffsetY;
        public final DoubleValue difficultyMeterTextScale;

        Client(ConfigSpecWrapper wrapper) {
            debugOverlayAnchor = wrapper
                    .builder("debug.overlay.anchor")
                    .comment("Position of debug overlay", EnumValue.allValuesComment(Anchor.class))
                    .defineEnum(Anchor.TOP_RIGHT);
            debugOverlayTextScale = wrapper
                    .builder("debug.overlay.textScale")
                    .comment("Overlay text size, where 1 is standard-sized text")
                    .defineInRange(0.75, 0.01, Double.MAX_VALUE);

            //region Health Hearts

            wrapper.comment("hearts.health.icons", "Settings for heart rows");

            heartIconStyle = wrapper
                    .builder("hearts.health.icons.style")
                    .comment("Heart style",
                            "REPLACE_ALL: All rows replaced with Scaling Health style hearts",
                            "REPLACE_AFTER_FIRST_ROW: Leave the first row vanilla style, Scaling Health style for additional rows",
                            "VANILLA: Do not change heart rendering (use this if you want another mod to handle heart rendering)")
                    .defineEnum(HeartIconStyle.REPLACE_ALL);
            heartColors = new ColorList(wrapper, "hearts.health.icons.colors",
                    "The color of each row of hearts. If the player has more rows than colors, it starts over from the beginning.",
                    0xBF0000, // 0 red
                    0xE66000, // 25 orange-red
                    0xE69900, // 40 orange
                    0xE6D300, // 55 yellow
                    0x99E600, // 80 lime
                    0x4CE600, // 100 green
                    0x00E699, // 160 teal
                    0x00E6E6, // 180 aqua
                    0x0099E6, // 200 sky blue
                    0x0000E6, // 240 blue
                    0x9900E6, // 280 dark purple
                    0xD580FF, // 280 light purple
                    0x8C8C8C, // 0 gray
                    0xE6E6E6  // 0 white
            );
            lastHeartOutline = wrapper
                    .builder("hearts.health.icons.lastHeartOutline")
                    .comment("The player's highest heart will get an outline around it.")
                    .define(true);
            lastHeartOutlineColor = wrapper
                    .builder("hearts.health.icons.lastHeartOutlineColor")
                    .comment("The color of the last heart outline, if enabled (see lastHeartOutline)")
                    .defineColorInt(Color.VALUE_WHITE);
            heartColorLooping = wrapper
                    .builder("hearts.health.icons.colorLooping")
                    .comment("If true, heart colors will 'loop around' to the first color after going through the",
                            "entire list. Set false to have every row after the last have the same color.")
                    .define(true);

            // Tanks
            heartTanks = wrapper
                    .builder("hearts.health.tanks.enabled")
                    .comment("Enable heart tanks, the small icons above your hearts which indicate the number of filled health rows")
                    .define(true);

            wrapper.comment("hearts.health.text", "Settings for the text displayed next to the heart rows");

            healthTextStyle = wrapper
                    .builder("hearts.health.text.style")
                    .comment("Style of health text", EnumValue.allValuesComment(HealthTextStyle.class))
                    .defineEnum(HealthTextStyle.ROWS);
            healthTextScale = wrapper
                    .builder("hearts.health.text.scale")
                    .comment("Health text scale, relative to its normal size (which varies by style)")
                    .defineInRange(1.0, 0.01, Double.MAX_VALUE);
            healthTextOffsetX = wrapper
                    .builder("hearts.health.text.offsetX")
                    .comment("Fine-tune text position")
                    .defineInRange(0, Integer.MIN_VALUE, Integer.MAX_VALUE);
            healthTextOffsetY = wrapper
                    .builder("hearts.health.text.offsetY")
                    .comment("Fine-tune text position")
                    .defineInRange(0, Integer.MIN_VALUE, Integer.MAX_VALUE);

            healthTextColorStyle = wrapper
                    .builder("hearts.health.text.color.style")
                    .comment("Health text color style.",
                            "TRANSITION: Gradually goes from full color to empty color as health is lost",
                            "PSYCHEDELIC: Taste the rainbow!",
                            "SOLID: Just stays at full color regardless of health")
                    .defineEnum(HealthTextColor.TRANSITION);
            healthTextFullColor = wrapper
                    .builder("hearts.health.text.color.full")
                    .comment("Color when health is full or style is SOLID")
                    .defineColorInt(0x4CFF4C);
            healthTextEmptyColor = wrapper
                    .builder("hearts.health.text.color.empty")
                    .comment("Color when health is empty and style is TRANSITION")
                    .defineColorInt(0xFF4C4C);

            //endregion

            //region Absorption Hearts

            absorptionIconStyle = wrapper
                    .builder("hearts.absorption.icons.style")
                    .comment("Style of absorption icons", EnumValue.allValuesComment(AbsorptionIconStyle.class))
                    .defineEnum(AbsorptionIconStyle.SHIELD);
            absorptionHeartColors = new ColorList(wrapper, "hearts.absorption.icons.colors",
                    "The color of each row of absorption hearts. If the player has more rows than colors, it starts over from the beginning.",
                    0xBF0000, // 0 red
                    0xE66000, // 25 orange-red
                    0xE69900, // 40 orange
                    0xE6D300, // 55 yellow
                    0x99E600, // 80 lime
                    0x4CE600, // 100 green
                    0x00E699, // 160 teal
                    0x00E6E6, // 180 aqua
                    0x0099E6, // 200 sky blue
                    0x0000E6, // 240 blue
                    0x9900E6, // 280 dark purple
                    0xD580FF, // 280 light purple
                    0x8C8C8C, // 0 gray
                    0xE6E6E6  // 0 white
            );

            EnumSet<HealthTextStyle> absorptionTextStyleValidValues = EnumSet.of(
                    HealthTextStyle.DISABLED, HealthTextStyle.HEALTH_ONLY, HealthTextStyle.ROWS);
            absorptionTextStyle = wrapper
                    .builder("hearts.absorption.text.style")
                    .comment("Style for absorption text. Because there is no 'max' value, the options are more limited.",
                            EnumValue.validValuesComment(absorptionTextStyleValidValues))
                    .defineEnum(HealthTextStyle.DISABLED, absorptionTextStyleValidValues);
            absorptionTextOffsetX = wrapper
                    .builder("hearts.absorption.text.offsetX")
                    .comment("Fine-tune text position")
                    .defineInRange(0, Integer.MIN_VALUE, Integer.MAX_VALUE);
            absorptionTextOffsetY = wrapper
                    .builder("hearts.absorption.text.offsetY")
                    .comment("Fine-tune text position")
                    .defineInRange(0, Integer.MIN_VALUE, Integer.MAX_VALUE);
            absorptionTextColor = wrapper
                    .builder("hearts.absorption.text.color")
                    .comment("The color of the absorption text")
                    .defineColorInt(Color.VALUE_WHITE);

            //endregion

            //region Difficulty

            warnWhenSleeping = wrapper
                    .builder("difficulty.warnWhenSleeping")
                    .comment("Display a warning to players trying to sleep, to remind them their difficulty may change. Sleeping is still allowed.")
                    .define(true);

            difficultyMeterShow = wrapper
                    .builder("difficulty.meter.show")
                    .comment("When to show the difficulty meter.",
                            " SOMETIMES will show the meter for a few seconds every so often.",
                            "KEYPRESS will make the meter keybinded",
                            "ALWAYS and NEVER should be obvious enough.")
                    .defineEnum(DifficultyMeterShow.SOMETIMES);
            difficultyMeterShowTime = wrapper
                    .builder("difficulty.meter.showDuration")
                    .comment("Show the difficulty meter for this many seconds (only on SOMETIMES mode)")
                    .defineInRange(8, 0, Double.MAX_VALUE);

            difficultyMeterAnchor = wrapper
                    .builder("difficulty.meter.position.anchor")
                    .comment("Position of the difficulty meter.", EnumValue.allValuesComment(Anchor.class))
                    .defineEnum(Anchor.BOTTOM_LEFT);
            difficultyMeterOffsetX = wrapper
                    .builder("difficulty.meter.position.offsetX")
                    .comment("Fine-tune the difficulty meter's position")
                    .defineInRange(5, Integer.MIN_VALUE, Integer.MAX_VALUE);
            difficultyMeterOffsetY = wrapper
                    .builder("difficulty.meter.position.offsetY")
                    .comment("Fine-tune the difficulty meter's position")
                    .defineInRange(-30, Integer.MIN_VALUE, Integer.MAX_VALUE);
            difficultyMeterTextScale = wrapper
                    .builder("difficulty.meter.text.scale")
                    .comment("Scale of text on the difficulty meter")
                    .defineInRange(0.6, 0, Double.MAX_VALUE);

            //endregion
        }
    }

    public static class Common {
        public final BooleanValue debugMaster;
        public final Supplier<Boolean> debugShowOverlay;
        public final Supplier<Boolean> debugLogEntitySpawns;
        public final Supplier<Boolean> debugLogScaledDamage;
        public final Supplier<Boolean> debugLogEquipmentEntries;

        public final BooleanValue crystalsAddHealth;
        public final BooleanValue xpAddHealth;
        public final BooleanValue crystalsRegenHealth;
        public final BooleanValue crystalsAddPetHealth;

        public final BooleanValue crystalsAddDamage;

        public final BooleanValue hpCrystalsOreGen;
        public final BooleanValue powerCrystalsOreGen;

        public final BooleanValue mobDamageIncrease;
        public final BooleanValue mobHpIncrease;

        public final BooleanValue playerDamageScaling;
        public final BooleanValue mobDamageScaling;

        public final BooleanValue enableDifficulty;
        public final BooleanValue enableBlights;

        Common(ConfigSpecWrapper wrapper) {
            wrapper.comment("features",
                    "All SH features can be disabled here. False to disable.");

            crystalsAddHealth = wrapper
                    .builder("features.crystalsAddHealth")
                    .comment("Enable player bonus hp by crystals.")
                    .define(true);

            xpAddHealth = wrapper
                    .builder("features.xpAddHealth")
                    .comment("Enable player bonus hp by xp.")
                    .define(true);

            crystalsRegenHealth = wrapper
                    .builder("features.crystalsRegenHealth")
                    .comment("Enable player regen hp by crystals.")
                    .define(true);

            crystalsAddPetHealth = wrapper
                    .builder("features.crystalsAddPetHealth")
                    .comment("Enable pet add hp by crystals.")
                    .define(true);

            crystalsAddDamage = wrapper
                    .builder("features.crystalsAddDamage")
                    .comment("Enable player add damage by crystals.")
                    .define(true);

            hpCrystalsOreGen = wrapper
                    .builder("features.hpCrystalsOreGen")
                    .comment("Enable ore gen of health crystals. Still drops as loot.")
                    .define(true);

            powerCrystalsOreGen = wrapper
                    .builder("features.powerCrystalsOreGen")
                    .comment("Enable ore gen of power crystals. Still drops as loot.")
                    .define(true);

            mobHpIncrease = wrapper
                    .builder("features.mobHpIncrease")
                    .comment("Mobs will gain bonus health with difficulty.")
                    .define(true);

            mobDamageIncrease = wrapper
                    .builder("features.mobDamageIncrease")
                    .comment("Mobs will gain bonus damage with difficulty.")
                    .define(true);

            playerDamageScaling = wrapper
                    .builder("features.playerDamageScaling")
                    .comment("Enable player damage scaling.")
                    .define(true);

            mobDamageScaling = wrapper
                    .builder("features.mobDamageScaling")
                    .comment("Enable mob damage scaling.")
                    .define(true);

            enableDifficulty = wrapper
                    .builder("features.enableDifficulty")
                    .comment("Enable difficulty system. If disabled, everything will have 0 difficulty.")
                    .define(true);

            enableBlights = wrapper
                    .builder("features.enableBlights")
                    .comment("Enable blights. If disabled, no blights will spawn.")
                    .define(true);



            wrapper.comment("debug",
                    "Debug settings are intended for tuning configs or diagnosing issues.",
                    "They may decrease performance and should be disabled for normal play.");

            debugMaster = wrapper
                    .builder("debug.masterSwitch")
                    .comment("Must be true for other debug settings to apply")
                    .define(false);
            debugShowOverlay = withMasterCheck(wrapper
                    .builder("debug.showOverlay")
                    .comment("Show some text in-game about player health, difficulty, and maybe other things.")
                    .define(true));
            debugLogEntitySpawns = withMasterCheck(wrapper
                    .builder("debug.logEntitySpawns")
                    .comment("Log details of entity spawns, including effects of difficulty.",
                            "This creates a lot of log spam, and will likely lag the game.")
                    .define(false));
            debugLogScaledDamage = withMasterCheck(wrapper
                    .builder("debug.logDamageScaling")
                    .comment("Log details of scaled damage, useful for fine-tuning damage scaling.",
                            "May create a fair amount of log spam, but shouldn't slow down the game too much.")
                    .define(true));
            debugLogEquipmentEntries = withMasterCheck(wrapper
                    .builder("debug.logEquipmentEntries")
                    .comment("Log details of equipment entries.",
                            "Likely going to lag slightly, mostly depending on the difficulty")
                    .define(true));
        }

        Supplier<Boolean> withMasterCheck(BooleanValue option) {
            return () -> debugMaster.get() && option.get();
        }
    }

    private Config() {}

    public static void init() {
        WRAPPER_CLIENT.validate();
        WRAPPER_COMMON.validate();
        GENERAL.validate();
    }

    private static Path resolve(Path parent, String path) {
        // Ensure directories exist
        File directory = parent.toFile();
        if (!directory.exists() && !directory.mkdirs()) {
            // Failed to create directories... should probably let this crash
            ScalingHealth.LOGGER.error("Failed to create config directory '{}'. This won't end well...",
                    directory.getAbsolutePath());
        }
        return parent.resolve(path);
    }
} public final class DamageScaling {
    private static final Marker MARKER = MarkerManager.getMarker("DamageScaling");

    public static final DamageScaling INSTANCE = new DamageScaling();

    private final Set<UUID> entityAttackedThisTick = new HashSet<>();

    private DamageScaling() {}

    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public void onEntityHurt(LivingAttackEvent event) {
        if(!EnabledFeatures.mobDamageScalingEnabled() && !EnabledFeatures.playerDamageScalingEnabled()) return;
        LivingEntity entity = event.getEntityLiving();
        if (entity.world.isRemote) return;
        // Entity invulnerable?
        if (entity.isInvulnerableTo(event.getSource()) || entity.hurtResistantTime > entity.maxHurtResistantTime / 2)
            return;

        // Check entity has already been processed from original event, or is not allowed to be affected
        if (entityAttackedThisTick.contains(entity.getUniqueID()) || !EntityGroup.from(entity).isAffectedByDamageScaling())
            return;

        DamageSource source = event.getSource();
        if (source == null) return;

        // Get scaling factor from map, if it exists. Otherwise, use the generic scale.
        float scale = (float) Config.GENERAL.damageScaling.getScale(source.getDamageType());

        // Get the amount of the damage to affect. Can be many times the base value.
        final float affectedAmount = (float) getEffectScale(entity);

        // Calculate damage to add to the original.
        final float original = event.getAmount();
        final float change = scale * affectedAmount * original;

        if (change > 0.0001f) {
            final float newAmount = makeSane(event.getAmount() + change);

            event.setCanceled(true);
            entityAttackedThisTick.add(entity.getUniqueID());
            entity.attackEntityFrom(event.getSource(), newAmount);

            if (Config.COMMON.debugLogScaledDamage.get()) {
                ScalingHealth.LOGGER.info(MARKER, "{} on {}: {} -> {} (scale={}, affected={}, change={})",
                        source.damageType, entity.getScoreboardName(), original, newAmount, scale, affectedAmount, change);
            }
        }
    }

    private static double getEffectScale(LivingEntity entity) {
        GameConfig config = Config.GENERAL;
        Mode mode = config.damageScaling.mode.get();
        switch (mode) {
            case AREA_DIFFICULTY:
                return SHDifficulty.areaDifficulty(entity.world, entity.getPosition()) * config.damageScaling.difficultyWeight.get();
            case MAX_HEALTH:
                double baseHealth = entity instanceof PlayerEntity
                        ? config.player.startingHealth.get()
                        : entity.getAttribute(SharedMonsterAttributes.MAX_HEALTH).getBaseValue();
                return (entity.getMaxHealth() - baseHealth) / baseHealth;
            case DIFFICULTY:
                return SHDifficulty.getDifficultyOf(entity) * config.damageScaling.difficultyWeight.get();
            default:
                throw new IllegalStateException("Unknown damage scaling mode: " + mode);
        }
    }

    private static float makeSane(float scaledAmount) {
        // Clamp scaled damage to sane values (non-negative and finite)
        if (scaledAmount < 0)
            return 0;
        if (!Float.isFinite(scaledAmount))
            return Float.MAX_VALUE;
        return scaledAmount;
    }

    @SubscribeEvent
    public void onServerTick(TickEvent.ServerTickEvent event) {
        entityAttackedThisTick.clear();
    }

    public enum Mode {
        MAX_HEALTH,
        DIFFICULTY,
        AREA_DIFFICULTY
    }
} @Mod.EventBusSubscriber(modid = ScalingHealth.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class PetEventHandler {
    public static PetEventHandler INSTANCE = new PetEventHandler();

    @SubscribeEvent
    public void onLivingUpdate(LivingUpdateEvent event) {
        double regenDelay = Config.GENERAL.pets.regenDelay.get();
        if (regenDelay <= 0) {
            return;
        }

        LivingEntity entity = event.getEntityLiving();
        if (entity != null && !entity.world.isRemote) {
            boolean fullHp = entity.getHealth() == entity.getMaxHealth();
            boolean isTamed = entity instanceof TameableEntity && ((TameableEntity) entity).isTamed();
            boolean isRegenTime = entity.hurtResistantTime <= 0 && entity.ticksExisted % regenDelay == 0;
            if (isTamed && isRegenTime && !fullHp) {
                entity.heal(2f);
            }
        }
    }

    @SubscribeEvent
    public void onPetInteraction(PlayerInteractEvent.EntityInteractSpecific event){
        if(!(event.getItemStack().getItem() instanceof HeartCrystal) ||
                !(event.getTarget() instanceof TameableEntity)       ||
                !EnabledFeatures.petBonusHpEnabled())
            return;

        TameableEntity pet = (TameableEntity) event.getTarget();
        if(!pet.isTamed()) {
            return;
        }
        if(pet.world.isRemote){
            event.setCancellationResult(ActionResultType.SUCCESS);
            event.setCanceled(true);
            return;
        }
        HeartCrystal heart = (HeartCrystal) event.getItemStack().getItem();
        heart.increasePetHp(event.getPlayer(), pet, event.getItemStack());
    }
} @Mod.EventBusSubscriber(modid = ScalingHealth.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public enum ModItems implements IItemProvider {
    HEART_CRYSTAL(HeartCrystal::new),
    HEART_CRYSTAL_SHARD(() -> new Item(new Item.Properties().group(ScalingHealth.SH))),
    HEART_DUST(() -> new Item(new Item.Properties().group(ScalingHealth.SH))),
    POWER_CRYSTAL(PowerCrystal::new),
    POWER_CRYSTAL_SHARD(() -> new Item(new Item.Properties().group(ScalingHealth.SH))),
    BANDAGES(() -> new HealingItem(0.3f, 1)),
    MEDKIT(() -> new HealingItem(0.7f, 4)),
    CURSED_HEART(() -> new DifficultyMutatorItem(DifficultyMutatorItem.Type.CURSED)),
    ENCHANTED_HEART(() -> new DifficultyMutatorItem(DifficultyMutatorItem.Type.ENCHANTED)),
    CHANCE_HEART(() -> new DifficultyMutatorItem(DifficultyMutatorItem.Type.CHANCE));

    private final Lazy<Item> item;

    ModItems(Supplier<Item> factory) {
        item = Lazy.of(factory);
    }

    @Override
    public Item asItem() {
        return item.get();
    }

    public String getName() {
        return name().toLowerCase(Locale.ROOT);
    }

    static final Collection<BlockItem> blocksToRegister = new ArrayList<>();

    @SubscribeEvent
    public static void registerAll(RegistryEvent.Register<Item> event) {
        blocksToRegister.forEach(ForgeRegistries.ITEMS::register);

        for (ModItems item : values()) {
            register(item.getName(), item.asItem());
        }
    }

    private static void register(String name, Item item) {
        ResourceLocation registryName = new ResourceLocation(ScalingHealth.MOD_ID, name);
        item.setRegistryName(registryName);
        ForgeRegistries.ITEMS.register(item);
    }
} public final class ModLoot {
    private ModLoot() {}

    public static void init() {
        MinecraftForge.EVENT_BUS.addListener(ModLoot::onLootTableLoad);

        LootConditionManager.registerCondition(EntityGroupCondition.SERIALIZER);
        LootConditionManager.registerCondition(SHMobProperties.SERIALIZER);
    }

    private static final List<ResourceLocation> ADD_ITEMS_TO = ImmutableList.of(
            LootTables.CHESTS_ABANDONED_MINESHAFT,
            LootTables.CHESTS_BURIED_TREASURE,
            LootTables.CHESTS_NETHER_BRIDGE,
            LootTables.CHESTS_SIMPLE_DUNGEON,
            LootTables.CHESTS_STRONGHOLD_LIBRARY,
            LootTables.CHESTS_UNDERWATER_RUIN_BIG,
            LootTables.CHESTS_WOODLAND_MANSION
    );

    private static void onLootTableLoad(LootTableLoadEvent event) {
        if (ADD_ITEMS_TO.contains(event.getName())) {
            event.getTable().addPool(correctEntries((new LootPool.Builder())
                    .name("scalinghealth_added")
                    .rolls(new RandomValueRange(1))
                    .addEntry(ItemLootEntry.builder(ModItems.HEART_CRYSTAL)
                            .weight(3)
                            .quality(2)
                            .acceptFunction(SetCount.builder(new RandomValueRange(1, 2)))
                    ))
                    .addEntry(EmptyLootEntry.func_216167_a().weight(10))
                    .build());
        }
    }

    private static LootPool.Builder correctEntries(LootPool.Builder builder){
        if(EnabledFeatures.difficultyEnabled())
            builder.addEntry(ItemLootEntry.builder(ModItems.CURSED_HEART)
                    .weight(1)
                    .quality(5)
                    .acceptFunction(SetCount.builder(new RandomValueRange(1, 3)))
                    )
                    .addEntry(ItemLootEntry.builder(ModItems.ENCHANTED_HEART)
                            .weight(1)
                            .quality(5)
                            .acceptFunction(SetCount.builder(new RandomValueRange(1, 3)))
                    )
                    .addEntry(ItemLootEntry.builder(ModItems.CHANCE_HEART)
                            .weight(1)
                            .quality(5)
                            .acceptFunction(SetCount.builder(new RandomValueRange(1, 3)))
                    );
        if(EnabledFeatures.powerCrystalEnabled())
            builder.addEntry(ItemLootEntry.builder(ModItems.POWER_CRYSTAL)
                    .weight(2)
                    .quality(7)
                    .acceptFunction(SetCount.builder(new RandomValueRange(1, 2)))
            );

        return builder;
    }
} public class TableGlobalModifier extends LootModifier {
   private final TableLootEntry table;

   protected TableGlobalModifier(ILootCondition[] conditions, TableLootEntry table) {
      super(conditions);
      this.table = table;
   }

   @Nonnull
   @Override
   protected List<ItemStack> doApply(List<ItemStack> generatedLoot, LootContext context) {
      table.func_216154_a(generatedLoot::add, context);
      generatedLoot.forEach(stack -> {
         if((EnabledFeatures.powerCrystalEnabled() && stack.getItem() instanceof PowerCrystal)
                 || (!EnabledFeatures.difficultyEnabled() && stack.getItem() instanceof DifficultyMutatorItem)){
            stack.setCount(0);
         }
      });
      return generatedLoot;
   }

   public static class Serializer extends GlobalLootModifierSerializer<TableGlobalModifier> {
      @Override
      public TableGlobalModifier read(ResourceLocation location, JsonObject object, ILootCondition[] lootConditions) {
         String resLoc = JSONUtils.getString(object, "table");
         TableLootEntry table = (TableLootEntry) TableLootEntry.builder(new ResourceLocation(resLoc)).build();
         return new TableGlobalModifier(lootConditions, table);
      }
   }
} public final class Network {
    private static final ResourceLocation NAME = ScalingHealth.getId("network");

    public static SimpleChannel channel;

    static {
        channel = NetworkRegistry.ChannelBuilder.named(NAME)
                .clientAcceptedVersions(s -> Objects.equals(s, "1"))
                .serverAcceptedVersions(s -> Objects.equals(s, "1"))
                .networkProtocolVersion(() -> "1")
                .simpleChannel();

        channel.messageBuilder(ClientSyncMessage.class, 1)
                .decoder(ClientSyncMessage::fromBytes)
                .encoder(ClientSyncMessage::toBytes)
                .consumer(ClientHandler::onMessage)
                .add();
        channel.messageBuilder(ClientLoginMessage.class, 2)
                .decoder(ClientLoginMessage::fromBytes)
                .encoder(ClientLoginMessage::toBytes)
                .consumer(ClientHandler::onLoginMessage)
                .add();
        channel.messageBuilder(SpawnBlightFirePacket.class, 3)
                .decoder(SpawnBlightFirePacket::fromBytes)
                .encoder(SpawnBlightFirePacket::toBytes)
                .consumer(SpawnBlightFirePacket::handle)
                .add();
    }

    private Network() {}

    public static void init() { }
} public class SHWorldFeatures {
    public static void addFeaturesToBiomes() {
        for (Biome biome : ForgeRegistries.BIOMES) {
            if(EnabledFeatures.hpCrystalsOreGenEnabled())
                addOre(biome, ModBlocks.HEART_CRYSTAL_ORE, 6, 1, 0, 28);
            if(EnabledFeatures.powerCrystalsOreGenEnabled())
                addOre(biome, ModBlocks.POWER_CRYSTAL_ORE, 6, 1, 0, 28);
        }
    }

    private static void addOre(Biome biome, IBlockProvider block, int size, int count, int minHeight, int maxHeight) {
        biome.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, Biome.createDecoratedFeature(
                Feature.ORE,
                new OreFeatureConfig(
                        OreFeatureConfig.FillerBlockType.NATURAL_STONE,
                        block.asBlockState(),
                        size
                ),
                Placement.COUNT_RANGE,
                new CountRangeConfig(count, minHeight, 0, maxHeight)
        ));
    }
}
import net.minecraft.item.ItemGroup;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.loot.GlobalLootModifierSerializer;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.DeferredWorkQueue;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.silentchaos512.scalinghealth.capability.DifficultyAffectedCapability;
import net.silentchaos512.scalinghealth.capability.DifficultySourceCapability;
import net.silentchaos512.scalinghealth.capability.PetHealthCapability;
import net.silentchaos512.scalinghealth.capability.PlayerDataCapability;
import net.silentchaos512.scalinghealth.client.KeyBinds.KeyManager;
import net.silentchaos512.scalinghealth.client.gui.DebugOverlay;
import net.silentchaos512.scalinghealth.client.gui.difficulty.DifficultyMeter;
import net.silentchaos512.scalinghealth.client.gui.health.HeartDisplayHandler;
import net.silentchaos512.scalinghealth.command.ModCommands;
import net.silentchaos512.scalinghealth.config.Config;
import net.silentchaos512.scalinghealth.event.DamageScaling;
import net.silentchaos512.scalinghealth.event.PetEventHandler;
import net.silentchaos512.scalinghealth.init.ModItems;
import net.silentchaos512.scalinghealth.init.ModLoot;
import net.silentchaos512.scalinghealth.loot.TableGlobalModifier;
import net.silentchaos512.scalinghealth.network.Network;
import net.silentchaos512.scalinghealth.world.SHWorldFeatures;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Optional;
import java.util.Random;
package net.silentchaos512.scalinghealth;



@Mod(ScalingHealth.MOD_ID)
public class ScalingHealth {
    public static final String MOD_ID = "scalinghealth";
    public static final String MOD_NAME = "Scaling Health";
    public static final String VERSION = "2.5.3";

    public static final Random random = new Random();

    public static final ItemGroup SH = new ItemGroup(MOD_ID) {
        @Override
        public ItemStack createIcon() {
            return new ItemStack(ModItems.HEART_CRYSTAL.asItem());
        }
    };

    public static final Logger LOGGER = LogManager.getLogger(MOD_NAME);

    public ScalingHealth() {
        Config.init();
        Network.init();
        ModLoot.init();

        FMLJavaModLoadingContext.get().getModEventBus().addListener(this::commonSetup);
        FMLJavaModLoadingContext.get().getModEventBus().addGenericListener(GlobalLootModifierSerializer.class, this::registerLootModSerializers);

        MinecraftForge.EVENT_BUS.addListener(this::serverAboutToStart);
        MinecraftForge.EVENT_BUS.register(PetEventHandler.INSTANCE);
        MinecraftForge.EVENT_BUS.register(DamageScaling.INSTANCE);
    }

    private void registerLootModSerializers(RegistryEvent.Register<GlobalLootModifierSerializer<?>> event) {
        event.getRegistry().register(new TableGlobalModifier.Serializer().setRegistryName(getId("table_loot_mod")));
    }

    private void commonSetup(FMLCommonSetupEvent event) {
        DifficultyAffectedCapability.register();
        DifficultySourceCapability.register();