package at.petrak.hexcasting.common.casting.operators.spells import at.petrak.hexcasting.api.casting.ParticleSpray import at.petrak.hexcasting.api.casting.RenderedSpell import at.petrak.hexcasting.api.casting.castables.SpellAction import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.getPlayer import at.petrak.hexcasting.api.casting.getPositiveDouble import at.petrak.hexcasting.api.casting.iota.Iota import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.api.pigment.FrozenPigment import at.petrak.hexcasting.api.player.FlightAbility import at.petrak.hexcasting.common.lib.HexItems import at.petrak.hexcasting.common.lib.HexSounds import at.petrak.hexcasting.xplat.IXplatAbstractions import net.minecraft.Util import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.sounds.SoundSource import net.minecraft.util.Mth import net.minecraft.world.item.DyeColor import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import kotlin.math.max import kotlin.math.roundToInt class OpFlight(val type: Type) : SpellAction { override val argc = 2 override fun execute( args: List, ctx: CastingEnvironment ): SpellAction.Result { val target = args.getPlayer(0, argc) val theArg = args.getPositiveDouble(1, argc) ctx.assertEntityInRange(target) val cost = when (this.type) { Type.LimitRange -> theArg * MediaConstants.DUST_UNIT // A second of flight should cost 1 shard Type.LimitTime -> theArg * MediaConstants.SHARD_UNIT }.roundToInt() // Convert to ticks return SpellAction.Result( Spell(this.type, target, theArg), cost, listOf(ParticleSpray(target.position(), Vec3(0.0, 2.0, 0.0), 0.0, 0.1)) ) } enum class Type { LimitRange, LimitTime; } data class Spell(val type: Type, val target: ServerPlayer, val theArg: Double) : RenderedSpell { override fun cast(ctx: CastingEnvironment) { if (target.abilities.mayfly) { // Don't accidentally clobber someone else's flight // TODO make this a mishap? return } val dim = target.level.dimension() val origin = target.position() val flight = when (this.type) { Type.LimitRange -> FlightAbility(-1, dim, origin, theArg) Type.LimitTime -> FlightAbility((theArg * 20.0).roundToInt(), dim, origin, -1.0) } IXplatAbstractions.INSTANCE.setFlight(target, flight) target.abilities.mayfly = true target.onUpdateAbilities() } } companion object { // blocks from the edge private val DIST_DANGER_THRESHOLD = 4.0 // seconds left private val TIME_DANGER_THRESHOLD = 7.0 * 20.0 @JvmStatic fun tickAllPlayers(world: ServerLevel) { for (player in world.players()) { tickDownFlight(player) } } @JvmStatic fun tickDownFlight(player: ServerPlayer) { val flight = IXplatAbstractions.INSTANCE.getFlight(player) if (flight != null) { val danger = getDanger(player, flight) if (danger >= 1.0) { IXplatAbstractions.INSTANCE.setFlight(player, null) // stop shin smashing bonke if (!player.isCreative && !player.isSpectator) { val abilities = player.abilities abilities.flying = false abilities.mayfly = false player.onUpdateAbilities() } player.level.playSound(null, player.x, player.y, player.z, HexSounds.FLIGHT_FINISH, SoundSource.PLAYERS, 2f, 1f) val superDangerSpray = ParticleSpray(player.position(), Vec3(0.0, 1.0, 0.0), Math.PI, 0.4, count = 20) superDangerSpray.sprayParticles(player.getLevel(), FrozenPigment(ItemStack(HexItems.DYE_COLORIZERS[DyeColor.RED]!!), Util.NIL_UUID)) superDangerSpray.sprayParticles(player.getLevel(), FrozenPigment(ItemStack(HexItems.DYE_COLORIZERS[DyeColor.BLACK]!!), Util.NIL_UUID)) } else { if (!player.abilities.mayfly) { player.abilities.mayfly = true player.onUpdateAbilities() } val time2 = if (flight.timeLeft >= 0) { flight.timeLeft - 1 } else { flight.timeLeft } IXplatAbstractions.INSTANCE.setFlight( player, FlightAbility( time2, flight.dimension, flight.origin, flight.radius ) ) val particleCount = 5 val dangerParticleCount = (particleCount * danger).roundToInt() val okParticleCount = particleCount - dangerParticleCount val oneDangerParticleCount = Mth.ceil(dangerParticleCount / 2.0) val color = IXplatAbstractions.INSTANCE.getColorizer(player) // TODO: have the particles go in the opposite direction of the velocity? ParticleSpray(player.position(), Vec3(0.0, -0.6, 0.0), 0.6, Math.PI * 0.3, count = okParticleCount) .sprayParticles(player.getLevel(), color) val dangerSpray = ParticleSpray(player.position(), Vec3(0.0, 1.0, 0.0), 0.3, Math.PI * 0.75, count = 0) dangerSpray.copy(count = oneDangerParticleCount) .sprayParticles(player.getLevel(), FrozenPigment(ItemStack(HexItems.DYE_COLORIZERS[DyeColor.BLACK]!!), Util.NIL_UUID)) dangerSpray.copy(count = oneDangerParticleCount) .sprayParticles(player.getLevel(), FrozenPigment(ItemStack(HexItems.DYE_COLORIZERS[DyeColor.RED]!!), Util.NIL_UUID)) if (player.level.random.nextFloat() < 0.02) player.level.playSound(null, player.x, player.y, player.z, HexSounds.FLIGHT_AMBIENCE, SoundSource.PLAYERS, 0.2f, 1f) if (flight.radius >= 0.0) { // Show the origin val spoofedOrigin = flight.origin.add(0.0, 1.0, 0.0) ParticleSpray(spoofedOrigin, Vec3(0.0, 1.0, 0.0), 0.5, Math.PI * 0.1, count = 5) .sprayParticles(player.getLevel(), color) ParticleSpray(spoofedOrigin, Vec3(0.0, -1.0, 0.0), 1.5, Math.PI * 0.25, count = 5) .sprayParticles(player.getLevel(), color) } } } } // Return a number from 0 (totally fine) to 1 (danger will robinson, stop the flight) // it's a double for particle reason private fun getDanger(player: ServerPlayer, flight: FlightAbility): Double { val radiusDanger = if (flight.radius >= 0.0) { if (player.level.dimension() != flight.dimension) { 1.0 } else { // Limit it only in X/Z val posXZ = Vec3(player.x, 0.0, player.z) val originXZ = Vec3(flight.origin.x, 0.0, flight.origin.z) val dist = posXZ.distanceTo(originXZ) val distFromEdge = flight.radius - dist if (distFromEdge >= DIST_DANGER_THRESHOLD) { 0.0 } else if (dist > flight.radius) { 1.0 } else { 1.0 - (distFromEdge / DIST_DANGER_THRESHOLD) } } } else 0.0 val timeDanger = if (flight.timeLeft >= 0) { if (flight.timeLeft >= TIME_DANGER_THRESHOLD) { 0.0 } else { val timeDanger = TIME_DANGER_THRESHOLD - flight.timeLeft timeDanger / TIME_DANGER_THRESHOLD } } else 0.0 return max(radiusDanger, timeDanger) } } }