package com.hypixel.hytale.builtin.npccombatactionevaluator; import com.hypixel.hytale.builtin.npccombatactionevaluator.config.CombatBalanceAsset; import com.hypixel.hytale.builtin.npccombatactionevaluator.evaluator.CombatActionEvaluator; import com.hypixel.hytale.builtin.npccombatactionevaluator.evaluator.CombatActionEvaluatorConfig; import com.hypixel.hytale.builtin.npccombatactionevaluator.evaluator.combatactions.CombatActionOption; import com.hypixel.hytale.builtin.npccombatactionevaluator.memory.DamageMemory; import com.hypixel.hytale.builtin.npccombatactionevaluator.memory.TargetMemory; import com.hypixel.hytale.builtin.npccombatactionevaluator.memory.TargetMemorySystems; import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Archetype; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.RemoveReason; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.dependency.Dependency; import com.hypixel.hytale.component.dependency.Order; import com.hypixel.hytale.component.dependency.SystemDependency; import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.HolderSystem; import com.hypixel.hytale.component.system.tick.EntityTickingSystem; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.math.random.RandomExtra; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.protocol.GameMode; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; import com.hypixel.hytale.server.core.modules.interaction.InteractionModule; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.npc.config.balancing.BalanceAsset; import com.hypixel.hytale.server.npc.decisionmaker.core.EvaluationContext; import com.hypixel.hytale.server.npc.decisionmaker.core.Evaluator; import com.hypixel.hytale.server.npc.entities.NPCEntity; import com.hypixel.hytale.server.npc.movement.controllers.MotionController; import com.hypixel.hytale.server.npc.role.Role; import com.hypixel.hytale.server.npc.role.support.StateSupport; import com.hypixel.hytale.server.npc.systems.BalancingInitialisationSystem; import com.hypixel.hytale.server.npc.systems.RoleSystems; import com.hypixel.hytale.server.npc.valuestore.ValueStore; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.util.List; import java.util.Set; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class CombatActionEvaluatorSystems { private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); public static class CombatConstructionData implements Component { protected String combatState; protected int markedTargetSlot; protected int minRangeSlot; protected int maxRangeSlot; protected int positioningAngleSlot; public static ComponentType getComponentType() { return NPCCombatActionEvaluatorPlugin.get().getCombatConstructionDataComponentType(); } public String getCombatState() { return this.combatState; } public void setCombatState(String state) { if (this.combatState != null && !this.combatState.equals(state)) { throw new IllegalStateException("Cannot have more than one combat state in an NPC!"); } else { this.combatState = state; } } public int getMarkedTargetSlot() { return this.markedTargetSlot; } public void setMarkedTargetSlot(int markedTargetSlot) { this.markedTargetSlot = markedTargetSlot; } public int getMinRangeSlot() { return this.minRangeSlot; } public void setMinRangeSlot(int minRangeSlot) { this.minRangeSlot = minRangeSlot; } public int getMaxRangeSlot() { return this.maxRangeSlot; } public void setMaxRangeSlot(int maxRangeSlot) { this.maxRangeSlot = maxRangeSlot; } public int getPositioningAngleSlot() { return this.positioningAngleSlot; } public void setPositioningAngleSlot(int positioningAngleSlot) { this.positioningAngleSlot = positioningAngleSlot; } @Nonnull @Override public Component clone() { CombatActionEvaluatorSystems.CombatConstructionData data = new CombatActionEvaluatorSystems.CombatConstructionData(); data.combatState = this.combatState; data.markedTargetSlot = this.markedTargetSlot; data.minRangeSlot = this.minRangeSlot; data.maxRangeSlot = this.maxRangeSlot; data.positioningAngleSlot = this.positioningAngleSlot; return data; } } public static class EvaluatorTick extends EntityTickingSystem { private final ComponentType componentType; private final ComponentType targetMemoryComponentType; private final ComponentType damageMemoryComponentType; @Nullable private final ComponentType npcComponentType; @Nonnull private final ComponentType playerComponentType; private final ComponentType valueStoreComponentType; private final ComponentType transformComponentType; private final Query query; @Nonnull private final Set> dependencies; public EvaluatorTick( ComponentType componentType, ComponentType targetMemoryComponentType, ComponentType damageMemoryComponentType ) { this.componentType = componentType; this.targetMemoryComponentType = targetMemoryComponentType; this.damageMemoryComponentType = damageMemoryComponentType; this.npcComponentType = NPCEntity.getComponentType(); this.playerComponentType = Player.getComponentType(); this.valueStoreComponentType = ValueStore.getComponentType(); this.transformComponentType = TransformComponent.getComponentType(); this.query = Archetype.of(componentType, targetMemoryComponentType, this.npcComponentType, this.valueStoreComponentType, this.transformComponentType); this.dependencies = Set.of( new SystemDependency<>(Order.BEFORE, RoleSystems.PreBehaviourSupportTickSystem.class), new SystemDependency<>(Order.AFTER, TargetMemorySystems.Ticking.class) ); } @Nonnull @Override public Set> getDependencies() { return this.dependencies; } @Override public Query getQuery() { return this.query; } @Override public boolean isParallel(int archetypeChunkSize, int taskCount) { return false; } @Override public void tick( float dt, int index, @Nonnull ArchetypeChunk archetypeChunk, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer ) { NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType); assert npcComponent != null; Role role = npcComponent.getRole(); if (!archetypeChunk.getArchetype().contains(DeathComponent.getComponentType())) { CombatActionEvaluator evaluatorComponent = archetypeChunk.getComponent(index, this.componentType); assert evaluatorComponent != null; evaluatorComponent.tickBasicAttackCoolDown(dt); StateSupport stateSupport = role.getStateSupport(); int currentState = stateSupport.getStateIndex(); if (currentState != evaluatorComponent.getRunInState()) { if (evaluatorComponent.getCurrentAction() != null) { evaluatorComponent.completeCurrentAction(true, true); evaluatorComponent.clearPrimaryTarget(); role.getMarkedEntitySupport().clearMarkedEntity(evaluatorComponent.getMarkedTargetSlot()); HytaleLogger.Api context = CombatActionEvaluatorSystems.LOGGER.at(Level.FINEST); if (context.isEnabled()) { context.log("%s: Leaving combat", archetypeChunk.getReferenceTo(index)); } } DamageMemory damageMemory = archetypeChunk.getComponent(index, this.damageMemoryComponentType); if (damageMemory != null) { damageMemory.clearTotalDamage(); } } else if (!role.getCombatSupport().isExecutingAttack()) { ValueStore valueStoreComponent = archetypeChunk.getComponent(index, this.valueStoreComponentType); assert valueStoreComponent != null; double[] postExecutionDistanceRange = evaluatorComponent.consumePostExecutionDistanceRange(); if (postExecutionDistanceRange != null) { valueStoreComponent.storeDouble(evaluatorComponent.getMinRangeSlot(), postExecutionDistanceRange[0]); valueStoreComponent.storeDouble(evaluatorComponent.getMaxRangeSlot(), postExecutionDistanceRange[1]); } int currentSubState = stateSupport.getSubStateIndex(); CombatActionEvaluatorConfig.BasicAttacks basicAttacks = evaluatorComponent.getBasicAttacks(currentSubState); if (basicAttacks != null) { evaluatorComponent.setCurrentBasicAttackSet(currentSubState, basicAttacks); String currentBasicAttack = evaluatorComponent.getCurrentBasicAttack(); if (currentBasicAttack != null) { if (!evaluatorComponent.tickBasicAttackTimeout(dt)) { role.getMarkedEntitySupport().setMarkedEntity(evaluatorComponent.getMarkedTargetSlot(), evaluatorComponent.getBasicAttackTarget()); return; } evaluatorComponent.clearCurrentBasicAttack(); HytaleLogger.Api context = CombatActionEvaluatorSystems.LOGGER.at(Level.FINEST); if (context.isEnabled()) { context.log("%s: Basic attack timed out", archetypeChunk.getReferenceTo(index)); } } if (evaluatorComponent.canUseBasicAttack(index, archetypeChunk, commandBuffer)) { MotionController activeMotionController = role.getActiveMotionController(); TransformComponent transformComponent = archetypeChunk.getComponent(index, this.transformComponentType); assert transformComponent != null; Vector3d position = transformComponent.getPosition(); Ref targetRef = null; CombatActionEvaluator.CombatOptionHolder currentAction = evaluatorComponent.getCurrentAction(); if (currentAction == null || ((CombatActionOption)currentAction.getOption()).getActionTarget() != CombatActionOption.Target.Friendly) { Ref primaryTargetRef = evaluatorComponent.getPrimaryTarget(); if (primaryTargetRef != null && primaryTargetRef.isValid()) { TransformComponent targetTransformComponent = commandBuffer.getComponent(primaryTargetRef, this.transformComponentType); assert targetTransformComponent != null; Vector3d targetPosition = targetTransformComponent.getPosition(); if (activeMotionController.getSquaredDistance(position, targetPosition, basicAttacks.shouldUseProjectedDistance()) < basicAttacks.getMaxRangeSquared()) { targetRef = primaryTargetRef; } } } if (targetRef == null) { TargetMemory targetMemoryComponent = archetypeChunk.getComponent(index, this.targetMemoryComponentType); assert targetMemoryComponent != null; targetRef = targetMemoryComponent.getClosestHostile(); } if (targetRef != null) { TransformComponent targetTransformComponentx = commandBuffer.getComponent(targetRef, this.transformComponentType); assert targetTransformComponentx != null; Vector3d targetPosition = targetTransformComponentx.getPosition(); if (activeMotionController.getSquaredDistance(position, targetPosition, basicAttacks.shouldUseProjectedDistance()) < basicAttacks.getMaxRangeSquared()) { evaluatorComponent.setBasicAttackTarget(targetRef); role.getMarkedEntitySupport().setMarkedEntity(evaluatorComponent.getMarkedTargetSlot(), targetRef); String[] basicAttackOptions = basicAttacks.getAttacks(); String attack; if (basicAttacks.isRandom()) { attack = basicAttackOptions[RandomExtra.randomRange(basicAttackOptions.length)]; } else { int nextAttackIndex = evaluatorComponent.getNextBasicAttackIndex(); attack = basicAttackOptions[nextAttackIndex]; if (++nextAttackIndex >= basicAttackOptions.length) { nextAttackIndex = 0; } evaluatorComponent.setNextBasicAttackIndex(nextAttackIndex); } evaluatorComponent.setCurrentBasicAttack(attack, basicAttacks.isDamageFriendlies(), basicAttacks::getInteractionVars); evaluatorComponent.setBasicAttackTimeout(basicAttacks.getTimeout()); HytaleLogger.Api context = CombatActionEvaluatorSystems.LOGGER.at(Level.FINEST); if (context.isEnabled()) { context.log("%s: Started basic attack %s", archetypeChunk.getReferenceTo(index), attack); } } } } } else { evaluatorComponent.setCurrentBasicAttackSet(currentSubState, null); } CombatActionEvaluator.CombatOptionHolder currentActionx = evaluatorComponent.getCurrentAction(); if (currentActionx != null) { if (((CombatActionOption)currentActionx.getOption()).getActionTarget() == CombatActionOption.Target.Self) { return; } if (!evaluatorComponent.hasTimedOut(dt)) { Ref targetRefx = evaluatorComponent.getPrimaryTarget(); if (targetRefx != null && targetRefx.isValid() && !commandBuffer.getArchetype(targetRefx).contains(DeathComponent.getComponentType())) { Player targetPlayerComponent = commandBuffer.getComponent(targetRefx, this.playerComponentType); if (targetPlayerComponent == null || targetPlayerComponent.getGameMode() == GameMode.Adventure) { role.getMarkedEntitySupport().setMarkedEntity(evaluatorComponent.getMarkedTargetSlot(), targetRefx); return; } } } evaluatorComponent.terminateCurrentAction(); evaluatorComponent.clearPrimaryTarget(); role.getMarkedEntitySupport().clearMarkedEntity(evaluatorComponent.getMarkedTargetSlot()); HytaleLogger.Api context = CombatActionEvaluatorSystems.LOGGER.at(Level.FINEST); if (context.isEnabled()) { context.log("%s: Lost current action target or timed out", archetypeChunk.getReferenceTo(index)); } } if (evaluatorComponent.getOptionsBySubState().containsKey(currentSubState)) { EvaluationContext evaluationContext = evaluatorComponent.getEvaluationContext(); double minRunUtility = evaluatorComponent.getMinRunUtility(); evaluationContext.setMinimumUtility(minRunUtility); evaluationContext.setMinimumWeightCoefficient(0.0); evaluationContext.setLastUsedNanos(evaluatorComponent.getLastRunNanos()); CombatActionEvaluator.RunOption runOption = evaluatorComponent.getRunOption(); double utility = runOption.calculateUtility(index, archetypeChunk, evaluatorComponent.getPrimaryTarget(), commandBuffer, evaluationContext); evaluationContext.reset(); if (!(utility < minRunUtility)) { Int2ObjectMap.OptionHolder>> optionLists = evaluatorComponent.getOptionsBySubState(); List.OptionHolder> currentStateOptions = (List.OptionHolder>)optionLists.get( currentSubState ); evaluatorComponent.setActiveOptions(currentStateOptions); evaluatorComponent.selectNextCombatAction(index, archetypeChunk, commandBuffer, role, valueStoreComponent); evaluatorComponent.setLastRunNanos(System.nanoTime()); DamageMemory damageMemory = archetypeChunk.getComponent(index, this.damageMemoryComponentType); if (damageMemory != null) { damageMemory.clearRecentDamage(); } HytaleLogger.Api context = CombatActionEvaluatorSystems.LOGGER.at(Level.FINEST); if (context.isEnabled()) { context.log("%s: Has run the combat action evaluator", archetypeChunk.getReferenceTo(index)); } } } } } } } public static class OnAdded extends HolderSystem { @Nullable private final ComponentType componentType = NPCEntity.getComponentType(); private final ComponentType combatConstructionDataComponentType; @Nonnull private final Set> dependencies; @Nonnull private final Query query; public OnAdded(ComponentType combatConstructionDataComponentType) { this.combatConstructionDataComponentType = combatConstructionDataComponentType; this.dependencies = Set.of(new SystemDependency<>(Order.AFTER, BalancingInitialisationSystem.class)); this.query = Query.and(combatConstructionDataComponentType, combatConstructionDataComponentType); } @Override public void onEntityAdd(@Nonnull Holder holder, @Nonnull AddReason reason, @Nonnull Store store) { Role role = holder.getComponent(this.componentType).getRole(); if (role.getBalanceAsset() != null) { BalanceAsset balancingAsset = BalanceAsset.getAssetMap().getAsset(role.getBalanceAsset()); if (balancingAsset instanceof CombatBalanceAsset combatBalance) { CombatActionEvaluatorSystems.CombatConstructionData constructionData = holder.getComponent(this.combatConstructionDataComponentType); CombatActionEvaluator evaluator = new CombatActionEvaluator(role, combatBalance.getEvaluatorConfig(), constructionData); evaluator.setupNPC(holder); role.getPositionCache().addExternalPositionCacheRegistration(evaluator::setupNPC); holder.putComponent(TargetMemory.getComponentType(), new TargetMemory(combatBalance.getTargetMemoryDuration())); holder.putComponent(CombatActionEvaluator.getComponentType(), evaluator); holder.ensureComponent(InteractionModule.get().getChainingDataComponent()); holder.removeComponent(this.combatConstructionDataComponentType); } } } @Override public void onEntityRemoved(@Nonnull Holder holder, @Nonnull RemoveReason reason, @Nonnull Store store) { } @Nonnull @Override public Query getQuery() { return this.query; } @Nonnull @Override public Set> getDependencies() { return this.dependencies; } } }