package com.mythlane.chainlightning.sceptre; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.protocol.InteractionType; import com.hypixel.hytale.server.core.entity.InteractionContext; import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler; import com.hypixel.hytale.server.core.modules.interaction.interaction.config.SimpleInstantInteraction; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.mythlane.chainlightning.chain.ChainHit; import com.mythlane.chainlightning.chain.ChainParameters; import com.mythlane.chainlightning.chain.ChainResolver; import com.mythlane.chainlightning.chain.Vec3; import javax.annotation.Nonnull; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** Runtime orchestrator: cooldown gate, chain resolution, damage, VFX emit, charge deduct. */ public final class ChainLightningSceptreInteraction extends SimpleInstantInteraction { private static final Logger LOGGER = Logger.getLogger(ChainLightningSceptreInteraction.class.getName()); private static final String COOLDOWN_ID = "chain_lightning_sceptre"; private static final float COOLDOWN_TIME = 4.0f; private static final float[] CHARGE_TIMES = { 4.0f }; private static final boolean FORCE_CREATE = true; private static final boolean INTERRUPT_RECHARGE = false; private static final double RAY_MAX_BLOCKS = 25.0; @Nonnull public static final BuilderCodec CODEC = ((BuilderCodec.Builder) BuilderCodec .builder(ChainLightningSceptreInteraction.class, ChainLightningSceptreInteraction::new, SimpleInstantInteraction.CODEC) .documentation( "Chain Lightning sceptre: primary/secondary click chains lightning to up to 5 targets.")) .build(); public ChainLightningSceptreInteraction() { } /** Runs the chain pipeline once per click; silently no-ops while on cooldown. */ @Override protected void firstRun(@Nonnull InteractionType type, @Nonnull InteractionContext context, @Nonnull CooldownHandler cooldownHandler) { CooldownHandler.Cooldown cooldown = cooldownHandler.getCooldown( COOLDOWN_ID, COOLDOWN_TIME, CHARGE_TIMES, FORCE_CREATE, INTERRUPT_RECHARGE); if (cooldown == null || cooldown.hasCooldown(false)) { return; } Ref playerRef = context.getEntity(); CommandBuffer commandBuffer = context.getCommandBuffer(); if (playerRef == null || commandBuffer == null) { return; } try { List hits = resolveChain(playerRef, commandBuffer); if (hits.isEmpty()) { return; } ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer); tryEmitVfx(hits, playerRef, commandBuffer); cooldown.deductCharge(); LOGGER.info(String.format("[ChainLightning] ref:%d chained %d targets", playerRef.getIndex(), hits.size())); } catch (Throwable t) { LOGGER.log(Level.WARNING, "[ChainLightning] chain pipeline failed", t); } } /** Builds the Hytale-bound adapters and runs the pure resolver against the live world. */ private static List resolveChain(@Nonnull Ref playerRef, @Nonnull CommandBuffer commandBuffer) { HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer); HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer); return ChainResolver.resolve( Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS, ray, neighbors, ChainParameters.DEFAULT ); } /** VFX emit is best-effort: damage is already applied so a failure must not abort the cooldown step. */ private static void tryEmitVfx(@Nonnull List hits, @Nonnull Ref playerRef, @Nonnull CommandBuffer commandBuffer) { try { HytaleVfxEmitter.playChainEffects(hits, playerRef, commandBuffer); } catch (Throwable t) { LOGGER.log(Level.WARNING, "[ChainLightning] vfx emit failed (damage already applied)", t); } } }