-
Notifications
You must be signed in to change notification settings - Fork 0
Ranged Example Walkthrough Skeleton
The bundled com.azure.azurecortex.example.skeleton.CortexSkeletonEntity shows the same integration shape as the
zombie in Full Example Walkthrough, applied to a ranged attacker. Read that page first if you have not already;
this page focuses on what is different: the bow/melee split inside HUNT_TARGET, item-use lifecycle callbacks, and the
small amount of vanilla skeleton behavior that must be disabled so it does not fight AzureCortex for control.
Vanilla Skeleton bookkeeping such as sunlight burning, drowning behavior, loot, and other base-entity behavior still
works normally. AzureCortex replaces the targeting/movement/attack decision layer.
The current 1.21.1 example adds two important pieces compared with the earlier walkthrough:
-
CortexSkeletonEntity#reassessWeaponGoal()is overridden as a no-op so vanilla cannot re-add its own bow/melee AI. -
UseItemActionnow hasonStartandonStoplifecycle callbacks, and the skeleton uses them to scopesetAggressive(true/false)exactly to the bow-use action.
That second change is what keeps the vanilla skeleton model in the correct bow pose while drawing without requiring the
entity to poll isUsingItem() every tick.
public enum CortexSkeletonGoal implements Goal {
NONE,
WANDER,
INVESTIGATE,
HUNT_TARGET;
@Override
public boolean isNone() {
return this == NONE;
}
}This remains intentionally simpler than the zombie example: there is no EAT_TO_HEAL emergency goal because this
example is meant to demonstrate ranged combat rather than planner complexity.
CortexSkeletonGoalPlanner follows a straightforward ordering:
- Hunt a valid live target.
- Otherwise investigate a recently seen target position.
- Otherwise wander.
Like the other examples, it uses GoalFailureCooldowns and recent PlanFeedback so repeated path/stuck failures can
reduce the score of immediately retrying the same hunt plan.
See GOAP Planning for the common planner machinery.
The skeleton's target sensor accepts vanilla-like hostile targets:
var validity = VanillaTargetPredicates.players()
.or(VanillaTargetPredicates.ironGolems())
.or(VanillaTargetPredicates.babyTurtlesOnLand());The sensor searches out to 24.0D, runs every 10 ticks, and applies line-of-sight filtering.
A periodic runtime hook refreshes CommonBlackboardKeys.DESTINATION from the live target every five ticks while
HUNT_TARGET is active, so the chase action is not following an old position.
Extending Skeleton gives the example useful vanilla behavior, but AbstractSkeleton also has logic that can install
its own RangedBowAttackGoal or MeleeAttackGoal when equipment changes.
That conflicts directly with AzureCortex's UseItemAction: both systems would try to control
startUsingItem() / stopUsingItem() and attack timing.
The current example therefore does this:
@Override
public void reassessWeaponGoal() {
// no-op: AzureCortex owns ranged/melee decision-making
}This is important even if registerGoals() itself contains only the small vanilla utility goals. Without the no-op,
vanilla can reintroduce weapon goals later through its normal skeleton equipment lifecycle.
HUNT_TARGET uses HuntTargetNode with two AttackProfiles:
- melee for point-blank range;
- bow for mid-to-long range.
AttackSelector re-evaluates the profiles every tick, so the mob can change attacks without a separate hand-written
combat state machine.
The chase fallback remains a normal MoveToDestinationAction using AStarPathfinder.INSTANCE.
The bow profile is built on UseItemAction's charge-and-release mode. The important pieces are:
-
readyToRelease: verifies there is still a living, visible target; -
onChargeTick: keeps the look controller aimed at that target while drawing; -
onRelease: explicitly performs the ranged attack; -
onStart: sets the skeleton aggressive while it begins using the bow; -
onStop: clears aggressive state whenever the action ends.
The current constructor shape is therefore conceptually:
new UseItemAction<>(
InteractionHand.MAIN_HAND,
agent -> agent.getMainHandItem().is(Items.BOW),
20,
40,
readyToRelease,
onChargeTick,
onRelease,
"skeleton_bow_cooldown",
20,
18,
(agent, bb) -> agent.setAggressive(true),
(agent, bb, reason) -> agent.setAggressive(false)
);The start/stop callbacks are optional in the generic action; they are used here because the vanilla skeleton renderer uses aggressive/item-use state as part of deciding how the arms should pose.
A vanilla bow has a non-obvious mob-specific gotcha.
Stopping item use on a non-player mob ends the draw animation, but that alone does not create the skeleton's arrow.
Vanilla's own ranged skeleton AI separately calls performRangedAttack(...) after stopping bow use.
UseItemAction intentionally remains generic rather than hardcoding bow behavior, so the skeleton does the same thing
from its release callback:
agent.performRangedAttack(target, BowItem.getPowerForTime(ticksCharged));That is the actual ranged attack. Without it, the skeleton can visibly draw/release the bow but never fire a projectile.
The same general hook is useful for other hold-then-release weapons whose non-player release behavior needs an explicit mob-side attack call.
Using an item does not automatically make AzureCortex's mob track the target visually. While the bow is held, the
example updates LookControl every tick so the skeleton keeps aiming instead of drawing the bow while staring in an
old direction.
This is separate from onStart/onStop:
-
onChargeTick= continuous aiming behavior; -
onStart/onStop= lifecycle state that should be true only while the action owns the bow use.
An earlier fix set aggressive state from the entity tick based on whether the skeleton was currently using a bow. The current action API makes that unnecessary.
UseItemAction#onStart runs immediately after startUsingItem(...). onStop is paired with it and runs when the action
ends, including success, failure, or behavior-tree interruption. That means temporary state can be scoped to the action
itself:
(agent, blackboard) -> agent.setAggressive(true)
(agent, blackboard, reason) -> agent.setAggressive(false)This is safer than external polling because cleanup still happens if another branch preempts the bow action halfway through its charge.
UseItemAction now cleanly supports two families of vanilla item use.
Use this when minChargeTicks > 0:
- call
startUsingItem(); - run
onStartonce; - call
onChargeTickwhile charging; - release once the minimum duration + release condition are satisfied, or at
maxChargeTicks; - call
stopUsingItem(); - call
onRelease; - finish and apply cooldown;
- call
onStopwhen the behavior node stops.
This is the mode used by the skeleton bow.
Use UseItemAction.autoComplete(...) for items such as food/drink whose vanilla use lifecycle completes on its own.
The action waits until isUsingItem() becomes false and then succeeds/applies cooldown. The charge-only callbacks are
not involved.
The example keeps the same basic bands:
- melee:
0.0Dto2.0D, priority25; - bow:
4.0Dto15.0D, priority20.
The 2.0D-to-4.0D gap is deliberate. When neither attack is legal, HuntTargetNode falls back to the chase action and
closes distance until an attack profile becomes available.
If you want a true kiting archer that backs away when the target gets too close, that is a different movement policy and should be represented by a custom combat/chase node rather than by these two range bands alone.
With aggressive pose state moved into UseItemAction callbacks, the entity tick remains focused on planning/runtime
work:
if (!level().isClientSide() && isAlive() && !isNoAi()) {
tickGoalPlanner();
runtime.tick();
}There is no longer a need for the skeleton entity to continuously derive setAggressive(...) from isUsingItem().
- Use
AttackProfile+AttackSelector+HuntTargetNodewhen an agent has multiple attacks selected by range, cooldown, and priority. - Use
UseItemAction's charge-and-release mode for held weapons that have a wind-up/charge period. - Put continuous targeting/aiming in
onChargeTick. - Put the actual non-player attack trigger in
onReleasewhen vanilla item release alone is insufficient. - Put temporary action-owned state in
onStartand always undo it inonStop. - If you extend a vanilla mob that dynamically re-adds its own weapon goals, disable that reinstallation path so it does not compete with AzureCortex.
- Full Example Walkthrough — the basic melee/zombie counterpart.
- Wall-Climbing Example (Spider) — the same broad GOAP/tree shape combined with crawl-aware navigation.
-
Built-in Actions — full reference for
UseItemAction,AttackProfile,AttackSelector, and related actions. -
Behavior Trees — how
HuntTargetNodeand the selectors decide which action runs.