BLOG

MODDING

7 MIN

The archer who fired before she drew

Laya is the AI companion I am building for Hytale. The first public version went up on CurseForge on 31 October 2026 as 0.4.5, and the page shows 252 downloads on one file. Locally she is already on 0.4.6, which is not out yet.

Most of the interesting bugs in a companion mod are not about intelligence. This one was about time. Watching her fight, one thing stood out: she shot an arrow, then pulled the bow back.

What it looked like, and what it was

The report was blunt and correct: she shoots first, then draws. That sends you looking at aim, range and target selection, when in fact nothing was out of order conceptually. The animation and the damage were simply decided in the same place.

In the plugin, a bow shot went through one function that played the draw animation and applied the hit in the same frame. For the engine both happened at once. For the player the hit and the arrow landed before the draw was readable, so the draw looked like a gesture that happened afterwards.

THE BUG WAS NOT THE ANIMATION An animation that looks wrong is usually not an animation bug. It is a bug in the order the game is told to do things. The animation played exactly as asked. It just played after the outcome.

Splitting one moment into two

Drawing a bow is two events on a screen: the body moves, then the arrow leaves. In code they were one call. The fix was to separate them rather than paper over the look.

A shot now starts the draw, spends the arrow and books what is in flight. The damage and the projectile happen one second later, when the engine has shown the release. Range, target and arrow count did not move. Only the timing did.

fireShot(target)          start the draw, spend the arrow, book the shot
drawTick(now)             1000 ms later: deal damage, spawn the arrow

A second is long in a fight, and that gap brought its own edge cases. Two mattered immediately:

  • The target can die during the draw. A target scheduled for damage a second after the shot is not the same world as the target you aimed at. At release, the entity is re-checked for validity and health before any damage is applied. A shot at something already down is dropped, and the arrow is not refunded, because the draw spent it.
  • She can be forced to move mid draw. If she still cannot hold her position shortly after the release point, the draw is cancelled, nothing fires, and the log says so plainly: released early, had to move. A companion that fires out of a cancelled draw is one you stop trusting.

The rhythm the bug was hiding

Once they were separate, the volley stopped being a blink. Each arrow starts its own draw, spaced by the release delay. Before, three arrows left 180 milliseconds apart with damage applied on the spot, which read as one invisible burst. Now it has a beat, and the beat is what makes it look like archery rather than a hitbox event.

A quieter fix rode along. Drawing is itself a signal that she is close enough, so it now holds her in place for the duration. Otherwise the gap between draws could be read by her movement code as a reason to back off, and she would shuffle away mid burst.

WHY I DO NOT REGENERATE A WHOLE FEATURE When a visual is wrong, the instinct is to rewrite the combat code or swap the model to something smarter. The actual cause was two lines of ordering. Reaching for the clever fix first would have buried a small, provable bug under a large, unprovable change.

The same mistake in a milder form, twice that week

Separating the draw and release stopped the animation looking backwards. It did not stop the underlying habit, which is why the next two versions were mostly corrections:

  • Shots into a corpse. A volley fires faster than the engine writes the kill. Two more arrows left at a target already on zero health because the down was recorded later in the tick than the shot. The fix was small and specific: refuse a target that is invalid or at zero health before spending an arrow.
  • A freeze after the fight. When the target went away, a flag saying she was holding at range was never cleared. The next hold skipped its stop command, a stale move order walked her a few blocks back, and a short freeze later the stuck watchdog teleported her to the player. Clearing the hold with the target fixed it.

The post fight freeze is worth naming because it did not look like a bug in a log. It looked like a bad decision. The watchdog saw a companion that had not moved, and did the right thing for a bad reason. That is the shape most companion bugs take: two correct subsystems, wrong about each other.

What I would tell another modder

In a real time game, the useful habit is not predicting correct behaviour. It is arranging the code so the order of events is visible.

  • Never let one function do two things that the player sees as two. A draw and a release, a swing and a hit, a step and a footfall. Split them at the same boundary the screen has.
  • Anything deferred needs a re-check at release. One second is long enough for the world to change, and the arrow is aimed at the past.
  • Log the gaps, not just the actions. Every one was diagnosed from a log line with a timestamp on it, not by reading the source.

None of that makes the AI better. It makes what you are looking at believable, which is the bar a companion must clear before the reasoning underneath matters. The first post covered the decision loop. This one is about the order it has to get right.

Laya Companion is on CurseForge, and the current stats and screenshots are on this site.