content: package com.alrex.parcool.common.action.impl;
  
  import com.alrex.parcool.api.SoundEvents;
  import com.alrex.parcool.client.animation.impl.HorizontalWallRunAnimator;
  import com.alrex.parcool.client.input.KeyBindings;
  import com.alrex.parcool.common.action.Action;
  import com.alrex.parcool.common.action.StaminaConsumeTiming;
  import com.alrex.parcool.common.capability.Animation;
  import com.alrex.parcool.common.capability.IStamina;
  import com.alrex.parcool.common.capability.Parkourability;
  import com.alrex.parcool.common.info.ActionInfo;
  import com.alrex.parcool.config.ParCoolConfig;
  import com.alrex.parcool.utilities.BufferUtil;
  import com.alrex.parcool.utilities.VectorUtil;
  import com.alrex.parcool.utilities.WorldUtil;
  import net.minecraft.block.BlockRenderType;
  import net.minecraft.block.BlockState;
  import net.minecraft.client.entity.player.ClientPlayerEntity;
  import net.minecraft.entity.ai.attributes.Attributes;
  import net.minecraft.entity.ai.attributes.ModifiableAttributeInstance;
  import net.minecraft.entity.player.PlayerEntity;
  import net.minecraft.particles.BlockParticleData;
  import net.minecraft.particles.ParticleTypes;
  import net.minecraft.util.math.BlockPos;
  import net.minecraft.util.math.vector.Vector2f;
  import net.minecraft.util.math.vector.Vector3d;
  import net.minecraft.world.World;
  import net.minecraftforge.api.distmarker.Dist;
  import net.minecraftforge.api.distmarker.OnlyIn;
  import net.minecraftforge.event.TickEvent;
  
  import java.nio.ByteBuffer;
  
  public class HorizontalWallRun extends Action {
  	public enum ControlType {
  		PressKey, Auto
  	}
  	private int coolTime = 0;
  	private float bodyYaw = 0;
  
  	private int getMaxRunningTick(ActionInfo info) {
          Integer value = info.getClientSetting().get(ParCoolConfig.Client.Integers.WallRunContinuableTick);
          if (value == null) value = ParCoolConfig.Client.Integers.WallRunContinuableTick.DefaultValue;
          return Math.min(value, info.getServerLimitation().get(ParCoolConfig.Server.Integers.MaxWallRunContinuableTick));
  	}
  
  	private boolean wallIsRightward = false;
  	private Vector3d runningWallDirection = null;
  	private Vector3d runningDirection = null;
  
  	@Override
  	public void onClientTick(PlayerEntity player, Parkourability parkourability, IStamina stamina) {
  		if (coolTime > 0) coolTime--;
  	}
  
  	@OnlyIn(Dist.CLIENT)
  	@Override
  	public void onWorkingTickInLocalClient(PlayerEntity player, Parkourability parkourability, IStamina stamina) {
  		Vector3d wallDirection = WorldUtil.getRunnableWall(player, player.getBbWidth() * 0.65f);
  		if (wallDirection == null) return;
  		if (runningWallDirection == null) return;
  		if (runningDirection == null) return;
  		Vector3d lookVec = VectorUtil.fromYawDegree(player.yBodyRot);
  		double differenceAngle = Math.asin(
  				new Vector3d(
  						lookVec.x() * runningDirection.x() + lookVec.z() * runningDirection.z(), 0,
  						-lookVec.x() * runningDirection.z() + lookVec.z() * runningDirection.x()
  				).normalize().z()
  		);
  		bodyYaw = (float) VectorUtil.toYawDegree(lookVec.yRot((float) (differenceAngle / 10)));
  		Vector3d movement = player.getDeltaMovement();
  		BlockPos leanedBlock = new BlockPos(
  				player.getX() + runningWallDirection.x(),
  				player.getBoundingBox().minY + player.getBbHeight() * 0.5,
  				player.getZ() + runningWallDirection.z()
  		);
  		if (!player.level.isLoaded(leanedBlock)) return;
  		float slipperiness = player.level.getBlockState(leanedBlock).getSlipperiness(player.level, leanedBlock, player);
  		if (slipperiness <= 0.8) {
  			double speedScale = 0.2;
  			ModifiableAttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
  			if (attr != null) {
  				speedScale *= attr.getValue() / attr.getBaseValue();
  			}
  			player.setDeltaMovement(
  					runningDirection.x() * speedScale,
  					movement.y() * (slipperiness - 0.1) * ((double) getDoingTick()) / getMaxRunningTick(parkourability.getActionInfo()),
  					runningDirection.z() * speedScale
  			);
  		}
  	}
  
  	@OnlyIn(Dist.CLIENT)
  	@Override
  	public boolean canStart(PlayerEntity player, Parkourability parkourability, IStamina stamina, ByteBuffer startInfo) {
  		Vector3d wallDirection = WorldUtil.getRunnableWall(player, player.getBbWidth() * 0.65f);
  		if (wallDirection == null) return false;
  		Vector3d wallVec = wallDirection.normalize();
  		Vector3d lookDirection = VectorUtil.fromYawDegree(player.yBodyRot);
  		lookDirection = new Vector3d(lookDirection.x(), 0, lookDirection.z()).normalize();
  		//doing "wallDirection/direction" as complex number(x + z i) to calculate difference of player's direction to steps
  		Vector3d dividedVec =
  				new Vector3d(
  						wallVec.x() * lookDirection.x() + wallVec.z() * lookDirection.z(), 0,
  						-wallVec.x() * lookDirection.z() + wallVec.z() * lookDirection.x()
  				).normalize();
  		if (Math.abs(dividedVec.z()) < 0.9) {
  			return false;
  		}
  		BufferUtil.wrap(startInfo).putBoolean(dividedVec.z() > 0/*if true, wall is in right side*/);
  		Vector3d runDirection = wallVec.yRot((float) (Math.PI / 2));
  		if (runDirection.dot(lookDirection) < 0) {
  			runDirection = runDirection.reverse();
  		}
  		startInfo.putDouble(wallDirection.x())
  				.putDouble(wallDirection.z())
  				.putDouble(runDirection.x())
  				.putDouble(runDirection.z());
  
  		return (!parkourability.get(WallJump.class).justJumped()
  				&& (
  				(ParCoolConfig.Client.HWallRunControl.get() == ControlType.PressKey && KeyBindings.getKeyHorizontalWallRun().isDown())
  						|| ParCoolConfig.Client.HWallRunControl.get() == ControlType.Auto
  		)
  				&& !parkourability.get(Crawl.class).isDoing()
  				&& !parkourability.get(Dodge.class).isDoing()
  				&& !parkourability.get(Vault.class).isDoing()
  				&& !parkourability.get(ClingToCliff.class).isDoing()
  				&& !player.isInWaterOrBubble()
  				&& Math.abs(player.getDeltaMovement().y()) < 0.5
  				&& coolTime == 0
  				&& !player.isOnGround()
  				&& parkourability.getAdditionalProperties().getNotLandingTick() > 5
  				&& (
  				parkourability.get(FastRun.class).canActWithRunning(player)
  						|| parkourability.get(FastRun.class).getNotDashTick(parkourability.getAdditionalProperties()) < 10
  		)
  				&& !stamina.isExhausted()
  		);
  	}
  
  	@OnlyIn(Dist.CLIENT)
  	@Override
  	public boolean canContinue(PlayerEntity player, Parkourability parkourability, IStamina stamina) {
  		Vector3d wallDirection = WorldUtil.getRunnableWall(player, player.getBbWidth() * 0.65f);
  		if (wallDirection == null) return false;
  		if (!(player instanceof ClientPlayerEntity)) return false;
  		ClientPlayerEntity localPlayer = (ClientPlayerEntity) player;
  		if (localPlayer.input == null) return false;
  		Vector2f moveVector = localPlayer.input.getMoveVector();
  		Vector3d actualInputVector
  				= new Vector3d(moveVector.x, 0, moveVector.y)
  				.normalize()
  				.yRot((float) -Math.toRadians(player.yRot));
  		// Input almost opposite to wall
  		if (wallDirection.normalize().dot(actualInputVector) < -0.86) {
  			return false;
  		}
  		return (getDoingTick() < getMaxRunningTick(parkourability.getActionInfo())
  				&& !stamina.isExhausted()
  				&& !parkourability.get(WallJump.class).justJumped()
  				&& !parkourability.get(Crawl.class).isDoing()
  				&& !parkourability.get(Dodge.class).isDoing()
  				&& !parkourability.get(Vault.class).isDoing()
  				&& ((ParCoolConfig.Client.HWallRunControl.get() == ControlType.PressKey && KeyBindings.getKeyHorizontalWallRun().isDown())
  				|| ParCoolConfig.Client.HWallRunControl.get() == ControlType.Auto)
  				&& !player.isOnGround()
  		);
  	}
  
  	@Override
  	public void onStop(PlayerEntity player) {
  		coolTime = 10;
  	}
  
  	@Override
  	public void onStartInLocalClient(PlayerEntity player, Parkourability parkourability, IStamina stamina, ByteBuffer startData) {
  		wallIsRightward = BufferUtil.getBoolean(startData);
  		runningWallDirection = new Vector3d(startData.getDouble(), 0, startData.getDouble());
  		runningDirection = new Vector3d(startData.getDouble(), 0, startData.getDouble());
  		if (ParCoolConfig.Client.Booleans.EnableActionSounds.get())
              player.playSound(SoundEvents.HORIZONTAL_WALL_RUN.get(), 1f, 1f);
  		Animation animation = Animation.get(player);
  		if (animation != null) {
  			animation.setAnimator(new HorizontalWallRunAnimator(wallIsRightward));
  		}
  	}
  
  	@Override
  	public void onStartInOtherClient(PlayerEntity player, Parkourability parkourability, ByteBuffer startData) {
  		wallIsRightward = BufferUtil.getBoolean(startData);
  		runningWallDirection = new Vector3d(startData.getDouble(), 0, startData.getDouble());
  		runningDirection = new Vector3d(startData.getDouble(), 0, startData.getDouble());
  		Animation animation = Animation.get(player);
  		if (ParCoolConfig.Client.Booleans.EnableActionSounds.get())
  			player.playSound(SoundEvents.HORIZONTAL_WALL_RUN.get(), 1f, 1f);
  		if (animation != null) {
  			animation.setAnimator(new HorizontalWallRunAnimator(wallIsRightward));
  		}
  	}
  
  	@OnlyIn(Dist.CLIENT)
  	@Override
  	public void onRenderTick(TickEvent.RenderTickEvent event, PlayerEntity player, Parkourability parkourability) {
  		if (isDoing()) {
  			if (runningDirection == null) return;
  			Vector3d lookVec = VectorUtil.fromYawDegree(player.getYHeadRot());
  			double differenceAngle = Math.asin(
  					new Vector3d(
  							lookVec.x() * runningDirection.x() + lookVec.z() * runningDirection.z(), 0,
  							-lookVec.x() * runningDirection.z() + lookVec.z() * runningDirection.x()
  					).normalize().z()
  			);
  			if (Math.abs(differenceAngle) > Math.PI / 4) {
  				player.yRot = ((float) VectorUtil.toYawDegree(
  						runningDirection.yRot((float) (-Math.signum(differenceAngle) * Math.PI / 4))
  				));
  			}
  			player.yBodyRotO = player.yBodyRot = bodyYaw;
  		}
  	}
  
  	@Override
  	public void onWorkingTickInClient(PlayerEntity player, Parkourability parkourability, IStamina stamina) {
  		spawnRunningParticle(player);
  	}
  
  	@Override
  	public void saveSynchronizedState(ByteBuffer buffer) {
  		buffer.putFloat(bodyYaw);
  	}
  
  	@Override
  	public void restoreSynchronizedState(ByteBuffer buffer) {
  		bodyYaw = buffer.getFloat();
  	}
  
  	@Override
  	public StaminaConsumeTiming getStaminaConsumeTiming() {
  		return StaminaConsumeTiming.OnWorking;
  	}
  
  	@OnlyIn(Dist.CLIENT)
  	public void spawnRunningParticle(PlayerEntity player) {
  		if (!ParCoolConfig.Client.Booleans.EnableActionParticles.get()) return;
  		if (runningDirection == null || runningWallDirection == null) return;
  		World level = player.level;
  		Vector3d pos = player.position();
  		BlockPos leanedBlock = new BlockPos(
  				pos.add(runningWallDirection.x(), player.getBbHeight() * 0.25, runningWallDirection.z())
  		);
  		if (!level.isLoaded(leanedBlock)) return;
  		float width = player.getBbWidth();
  		BlockState blockstate = level.getBlockState(leanedBlock);
  
  		Vector3d wallDirection = runningWallDirection.normalize();
  		Vector3d orthogonalToWallVec = wallDirection.yRot((float) (Math.PI / 2));
  		Vector3d particleBaseDirection = runningDirection.subtract(wallDirection);
  		if (blockstate.getRenderShape() != BlockRenderType.INVISIBLE) {
  			Vector3d particlePos = new Vector3d(
  					pos.x() + (wallDirection.x() * 0.4 + orthogonalToWallVec.x() * (player.getRandom().nextDouble() - 0.5D)) * width,
  					pos.y() + 0.1D + 0.3 * player.getRandom().nextDouble(),
  					pos.z() + (wallDirection.z() * 0.4 + orthogonalToWallVec.z() * (player.getRandom().nextDouble() - 0.5D)) * width
  			);
  			Vector3d particleSpeed = particleBaseDirection
  					.yRot((float) (Math.PI * 0.2 * (player.getRandom().nextDouble() - 0.5)))
  					.scale(3 + 6 * player.getRandom().nextDouble())
  					.add(0, 1.5, 0);
  			level.addParticle(
  					new BlockParticleData(ParticleTypes.BLOCK, blockstate).setPos(leanedBlock),
  					particlePos.x(),
  					particlePos.y(),
  					particlePos.z(),
  					particleSpeed.x(),
  					particleSpeed.y(),
  					particleSpeed.z()
  			);
  		}
  	}
  }
  