Skip to content

fix(monster): prevent spiral movement loop when overlaps creature#184

Merged
ranisalt merged 7 commits intodevfrom
fix/monster-spiral-movement
Mar 2, 2026
Merged

fix(monster): prevent spiral movement loop when overlaps creature#184
ranisalt merged 7 commits intodevfrom
fix/monster-spiral-movement

Conversation

@ramon-bernardo
Copy link
Copy Markdown
Member

@ramon-bernardo ramon-bernardo commented Feb 17, 2026

Pull Request Prelude

Changes Proposed

Fixes a movement bug where monsters would enter a permanent "spiral loop" and lose their smooth walking ability when a player stood directly on top of them.

Previously, the nested logic in goToFollowCreature would get stuck in a state where it only performed jittery, step-by-step movements. Now, the logic to use a simplified simpleStep check and ensuring a clean fallback to the A* pathfinder (updateFollowCreaturePath), the creature can now correctly recalculate its route out of an overlap. This restores natural movement flow and prevents the "step-by-step" movement error.

Introduced in otland#4811
Issue address otland#4865

Summary by CodeRabbit

  • Bug Fixes
    • More reliable creature-following: short/simple movement steps used when appropriate for quicker adjustments.
    • Follow completion handling unified so follow actions consistently trigger target-list updates.
    • Target ordering now updates reliably after either simple moves or full path updates, improving follow prioritization and consistency.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

goToFollowCreature() gains a short “simple step” branch that may perform a single-distance step and calls startAutoWalk(direction); both the simple-step and the path-update flows invoke a new Monster::onFollowCreatureComplete() hook to reorder or remove the follow-target from targetList.

Changes

Cohort / File(s) Summary
Follow Creature Logic
src/monster.cpp, src/monster.h
Added a simpleStep branch in goToFollowCreature() that uses getDistanceStep and may call startAutoWalk(direction); introduced Monster::onFollowCreatureComplete() and call it from both the simple-step and the existing path-update paths to centralize targetList reordering/erasure.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • ranisalt

Poem

🐰 I hopped a tidy, measured pace,
A tiny step to keep the chase,
Targets shuffled, front or flown,
One quick nudge and order shown,
Thump-thump — a rabbit's quiet grace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(monster): prevent spiral movement loop when overlaps creature' directly addresses the core issue being fixed—preventing monsters from entering a spiral movement loop when overlapped with another creature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/monster-spiral-movement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/monster.cpp (1)

550-564: ⚠️ Potential issue | 🔴 Critical

Confirm: asymmetric targetList handling for summons causes target loss.

The concern in the original review is valid. selectTarget() requires the target to already exist in targetList (it returns false at line 612 if the target is not found via find_if). When onFollowCreatureComplete() erases a summon's target, there is no re-add mechanism:

  • onThinkTarget() skips summons entirely (if (!isSummon()) at line 870), so summons never call searchTarget() to repopulate targets.
  • A summon's only attempt to add targets is at line 731: selectTarget(master->getAttackedCreature()), which fails silently if the target was erased and is no longer in targetList.

This creates the asymmetry: non-summons use iter_swap to deprioritize but retain targets in the list, while summons erase entirely with no recovery path. A summon can permanently lose a target if it is erased during pursuit and the master's attack target remains the same.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` around lines 550 - 564, The current
onFollowCreatureComplete() erases a summon’s target from targetList which
prevents selectTarget() from finding/re-using it (and onThinkTarget() never
calls searchTarget() for summons), causing permanent target loss; change the
summon branch in Monster::onFollowCreatureComplete() to preserve the target
ordering instead of erasing (e.g., use std::iter_swap(it, targetList.end()-1)
like the non-summon branch, with proper handling when targetList has size 1) so
summons retain the target entry, or alternatively push the target to the back
rather than erase it; update any edge-case handling so selectTarget(),
onThinkTarget(), and searchTarget() can continue to operate on the preserved
entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/monster.cpp`:
- Line 530: The line declaring the variable Direction dir = DIRECTION_NONE; has
two extra tabs; fix the clang-format error by removing the extra indentation so
the declaration aligns with the surrounding function body indentation (match
other local variable declarations in the same function), e.g., make the start of
"Direction dir = DIRECTION_NONE;" align with neighboring statements in the
function.

---

Outside diff comments:
In `@src/monster.cpp`:
- Around line 550-564: The current onFollowCreatureComplete() erases a summon’s
target from targetList which prevents selectTarget() from finding/re-using it
(and onThinkTarget() never calls searchTarget() for summons), causing permanent
target loss; change the summon branch in Monster::onFollowCreatureComplete() to
preserve the target ordering instead of erasing (e.g., use std::iter_swap(it,
targetList.end()-1) like the non-summon branch, with proper handling when
targetList has size 1) so summons retain the target entry, or alternatively push
the target to the back rather than erase it; update any edge-case handling so
selectTarget(), onThinkTarget(), and searchTarget() can continue to operate on
the preserved entry.

Comment thread src/monster.cpp Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/monster.cpp (2)

532-532: isFleeing() is redundant in the simpleStep condition.

getPathSearchParams (called just above at line 528) sets fpp.maxTargetDist = Map::maxViewportX for non-summon fleeing monsters (line 1978), which is always > 1. So isFleeing() || fpp.maxTargetDist > 1 always reduces to fpp.maxTargetDist > 1 — the isFleeing() arm can never fire independently.

♻️ Suggested simplification
-	const auto simpleStep = !isSummon() && (isFleeing() || fpp.maxTargetDist > 1);
+	const auto simpleStep = !isSummon() && fpp.maxTargetDist > 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` at line 532, The condition for simpleStep is redundant
because getPathSearchParams sets fpp.maxTargetDist = Map::maxViewportX (>1) for
non-summon fleeing monsters, so remove the unnecessary isFleeing() check: change
the expression that defines simpleStep (in the block using getPathSearchParams)
to rely solely on !isSummon() && (fpp.maxTargetDist > 1) and ensure no other
logic depends on isFleeing() being evaluated there; update the variable
definition that currently references isFleeing() and fpp.maxTargetDist
accordingly.

546-560: onFollowCreatureComplete should be declared private in monster.h.

The method is only called from goToFollowCreature() within the same class and exposing it as public unnecessarily widens the API surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` around lines 546 - 560, The method onFollowCreatureComplete
is currently exposed publicly but is only invoked from within the class
(goToFollowCreature), so update the Monster class declaration in monster.h to
move the onFollowCreatureComplete() declaration into the private section (or add
a private: label and place it there), ensuring the signature matches the
implementation in src/monster.cpp and no other callers rely on it being public;
recompile to verify access changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/monster.cpp`:
- Line 530: The previous extra indentation and naming issue has been resolved;
ensure the local variable is declared as "auto direction = DIRECTION_NONE;" with
a single-tab indentation level (not "Direction dir" or extra tabs) where the
monster direction is initialized so the symbol direction is consistently used
throughout the surrounding functions (e.g., any uses of direction in movement
logic or checks should reference this renamed variable).

---

Nitpick comments:
In `@src/monster.cpp`:
- Line 532: The condition for simpleStep is redundant because
getPathSearchParams sets fpp.maxTargetDist = Map::maxViewportX (>1) for
non-summon fleeing monsters, so remove the unnecessary isFleeing() check: change
the expression that defines simpleStep (in the block using getPathSearchParams)
to rely solely on !isSummon() && (fpp.maxTargetDist > 1) and ensure no other
logic depends on isFleeing() being evaluated there; update the variable
definition that currently references isFleeing() and fpp.maxTargetDist
accordingly.
- Around line 546-560: The method onFollowCreatureComplete is currently exposed
publicly but is only invoked from within the class (goToFollowCreature), so
update the Monster class declaration in monster.h to move the
onFollowCreatureComplete() declaration into the private section (or add a
private: label and place it there), ensuring the signature matches the
implementation in src/monster.cpp and no other callers rely on it being public;
recompile to verify access changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/monster.cpp (1)

532-535: getDistanceStep can return true with direction still DIRECTION_NONE in rare diagonal dead-end cases.

In getDistanceStep, several diagonal-escape branches (e.g. lines 1367–1373, 1415–1421, etc.) set direction only if two compound canWalkTo conditions hold, then unconditionally return true — so if none of those conditions match, direction remains DIRECTION_NONE.

With auto direction = DIRECTION_NONE, calling startAutoWalk(direction) then sets listWalkDir = {DIRECTION_NONE} and queues a walk event that produces a wasted tick (creature doesn't actually move). Recovery happens on the next think cycle, so the impact is a brief hesitation rather than a hard break. This is a pre-existing quirk in getDistanceStep, but it is newly exercised by this code path.

Consider guarding:

🛡️ Proposed guard
-		if (getDistanceStep(followCreature->getPosition(), direction, isFleeing())) {
+		if (getDistanceStep(followCreature->getPosition(), direction, isFleeing()) && direction != DIRECTION_NONE) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` around lines 532 - 535, The call site assumes
getDistanceStep sets a valid direction but getDistanceStep can return true while
leaving direction as DIRECTION_NONE; update the follow logic to guard against
that: after calling getDistanceStep(followCreature->getPosition(), direction,
isFleeing()) only set hasFollowPath = true and call startAutoWalk(direction)
when direction != DIRECTION_NONE (keep the existing variables: direction,
hasFollowPath, startAutoWalk, followCreature), otherwise do not enqueue a
DIRECTION_NONE walk and let the next think cycle retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/monster.cpp`:
- Around line 532-535: The call site assumes getDistanceStep sets a valid
direction but getDistanceStep can return true while leaving direction as
DIRECTION_NONE; update the follow logic to guard against that: after calling
getDistanceStep(followCreature->getPosition(), direction, isFleeing()) only set
hasFollowPath = true and call startAutoWalk(direction) when direction !=
DIRECTION_NONE (keep the existing variables: direction, hasFollowPath,
startAutoWalk, followCreature), otherwise do not enqueue a DIRECTION_NONE walk
and let the next think cycle retry.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/monster.cpp (1)

553-556: ⚠️ Potential issue | 🟡 Minor

Verify intentionality of hard-erasing follow target from targetList for summons on path failure.

This erase branch is new behavior introduced by this PR. When a summon fails A* pathing (!hasFollowPath), its follow target is permanently removed from targetList, whereas non-summons (the else if branch) merely rotate it to the back of the list. Since selectTarget() requires the creature to be in targetList, erasing it prevents immediate re-selection via the normal targeting flow.

While a re-population path exists—onCreatureFound() can re-add the target via addTarget() if it re-enters visibility—this requires the creature to be discovered again and pass the isOpponent() check. For summons ordered to attack an enemy that becomes temporarily unreachable (e.g., obstacle between summon and target), this erase behavior is stricter than non-summons and could cause unnecessary target abandonment if re-discovery is delayed or the target doesn't re-enter the summon's awareness radius.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` around lines 553 - 556, The current branch removes a
summon’s follow target from targetList on failed pathing (targetList.erase(it)),
which prevents selectTarget() from reselecting it; change this to match the
non-summon behavior by moving the entry to the back (use std::iter_swap(it,
targetList.end() - 1)) instead of erasing, so hasFollowPath failure for
isSummon() doesn’t permanently drop the target; update the block handling
isSummon() and ensure related flows (selectTarget(), onCreatureFound(),
addTarget(), hasFollowPath) keep consistent semantics.
🧹 Nitpick comments (1)
src/monster.cpp (1)

530-531: simpleStep condition simplifies to !isSummon() in practice.

fpp.maxTargetDist is set to mType->info.targetDistance in getPathSearchParams (minimum 1 for any configured monster), making fpp.maxTargetDist >= 1 a tautology for virtually all monster types. The isFleeing() || clause is equally redundant since fleeing monsters have fpp.maxTargetDist = Map::maxViewportX. The condition could be simplified to !isSummon(), or tightened with a more explicit predicate (e.g., distance == 0) if the intent is specifically to handle the overlap case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` around lines 530 - 531, The current simpleStep check is
effectively always true when not a summon; replace the tautological condition by
either (A) simplifying it to const auto simpleStep = !isSummon(); if the
original intent was just to exclude summons, or (B) tightening it to explicitly
detect the overlap case, e.g. const auto simpleStep = !isSummon() && distance ==
0; if you meant to only handle zero-distance overlap; update the line that
defines simpleStep (and adjust any dependent logic) accordingly—refer to
simpleStep, isSummon(), isFleeing(), fpp.maxTargetDist, and
getPathSearchParams/mType->info.targetDistance when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/monster.cpp`:
- Around line 533-537: After getDistanceStep(...) returns true, check the
direction value and avoid calling startAutoWalk when it is DIRECTION_NONE: if
getDistanceStep(followCreature->getPosition(), direction, isFleeing()) returns
true but direction == DIRECTION_NONE, call onFollowCreatureComplete() and return
without invoking startAutoWalk or queuing a walk; otherwise proceed with
hasFollowPath = true and startAutoWalk(direction). This prevents DIRECTION_NONE
from being pushed into listWalkDir and later passed to getNextPosition.

---

Outside diff comments:
In `@src/monster.cpp`:
- Around line 553-556: The current branch removes a summon’s follow target from
targetList on failed pathing (targetList.erase(it)), which prevents
selectTarget() from reselecting it; change this to match the non-summon behavior
by moving the entry to the back (use std::iter_swap(it, targetList.end() - 1))
instead of erasing, so hasFollowPath failure for isSummon() doesn’t permanently
drop the target; update the block handling isSummon() and ensure related flows
(selectTarget(), onCreatureFound(), addTarget(), hasFollowPath) keep consistent
semantics.

---

Nitpick comments:
In `@src/monster.cpp`:
- Around line 530-531: The current simpleStep check is effectively always true
when not a summon; replace the tautological condition by either (A) simplifying
it to const auto simpleStep = !isSummon(); if the original intent was just to
exclude summons, or (B) tightening it to explicitly detect the overlap case,
e.g. const auto simpleStep = !isSummon() && distance == 0; if you meant to only
handle zero-distance overlap; update the line that defines simpleStep (and
adjust any dependent logic) accordingly—refer to simpleStep, isSummon(),
isFleeing(), fpp.maxTargetDist, and
getPathSearchParams/mType->info.targetDistance when making the change.

Comment thread src/monster.cpp
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/monster.cpp`:
- Around line 530-545: The follow logic fails to mark a successful follow when
getDistanceStep(...) returns true but leaves direction == DIRECTION_NONE
(monster already at targetDistance); update the simpleStep branch in the follow
routine so hasFollowPath is set to true whenever getDistanceStep(...) returns
true (regardless of direction) before calling onFollowCreatureComplete(),
keeping the existing startAutoWalk(direction) call only when direction !=
DIRECTION_NONE; adjust the block around getDistanceStep, DIRECTION_NONE,
hasFollowPath, startAutoWalk and onFollowCreatureComplete to ensure being
exactly at targetDistance is treated as a completed follow.

Comment thread src/monster.cpp Outdated
Comment on lines 530 to 545
const auto simpleStep = !isSummon() && (isFleeing() || fpp.maxTargetDist >= 1);
if (simpleStep) {
auto direction = DIRECTION_NONE;
if (getDistanceStep(followCreature->getPosition(), direction, isFleeing())) {
if (direction != DIRECTION_NONE) {
hasFollowPath = true;
startAutoWalk(direction);
}
}

if (dir != DIRECTION_NONE) {
listWalkDir.clear();
listWalkDir.push_back(dir);

hasFollowPath = true;
startAutoWalk();
onFollowCreatureComplete();
return;
}
} else {
updateFollowCreaturePath(fpp);
}

updateFollowCreaturePath(fpp);
onFollowCreatureComplete();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how hasFollowPath is used in context
echo "=== hasFollowPath assignments in src/monster.cpp ==="
rg -n 'hasFollowPath\s*=' src/monster.cpp -B1 -A1

echo ""
echo "=== hasFollowPath usage in onFollowCreatureComplete ==="
rg -n 'onFollowCreatureComplete' src/monster.cpp -A15 | head -30

echo ""
echo "=== onThink context around searchTarget ==="
rg -n 'searchTarget' src/monster.cpp -B3 -A1

echo ""
echo "=== getDistanceStep implementation ==="
rg -n 'getDistanceStep' src/monster.cpp -A20 | head -40

Repository: atlas-kit/atlas

Length of output: 3219


🏁 Script executed:

#!/bin/bash
echo "=== All hasFollowPath references in codebase ==="
rg -n 'hasFollowPath' src/ -B1 -A1

echo ""
echo "=== Check if hasFollowPath is a member variable ==="
rg -n 'bool hasFollowPath' src/ -B1 -A1

echo ""
echo "=== Check initialization in constructor ==="
rg -n 'Monster::Monster' src/monster.cpp -A30 | head -50

Repository: atlas-kit/atlas

Length of output: 3810


hasFollowPath is not set when the monster reaches exactly targetDistance.

When getDistanceStep() returns true with direction remaining DIRECTION_NONE (line 1307–1309: the monster is at mType->info.targetDistance), the code skips the hasFollowPath = true assignment and proceeds to onFollowCreatureComplete(). This leaves hasFollowPath in its previous state.

Downstream consequences:

  • onFollowCreatureComplete() (line 557) swaps the target to the end of the list when hasFollowPath is false, demoting an already-reached target.
  • onThink() (line 741–742) calls searchTarget() when !hasFollowPath, triggering unnecessary target switches despite the monster being at the desired distance.
  • getNextStep() (line 1144–1151) treats !hasFollowPath as needing random stepping instead of dance-step logic.
  • Summon spawning (line 939) is prevented when hasFollowPath is false.

Since being at targetDistance is a successful follow state, set hasFollowPath = true unconditionally when getDistanceStep() returns true:

Suggested fix
 		if (getDistanceStep(followCreature->getPosition(), direction, isFleeing())) {
+			hasFollowPath = true;
 			if (direction != DIRECTION_NONE) {
-				hasFollowPath = true;
 				startAutoWalk(direction);
 			}
 
 			onFollowCreatureComplete();
 			return;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monster.cpp` around lines 530 - 545, The follow logic fails to mark a
successful follow when getDistanceStep(...) returns true but leaves direction ==
DIRECTION_NONE (monster already at targetDistance); update the simpleStep branch
in the follow routine so hasFollowPath is set to true whenever
getDistanceStep(...) returns true (regardless of direction) before calling
onFollowCreatureComplete(), keeping the existing startAutoWalk(direction) call
only when direction != DIRECTION_NONE; adjust the block around getDistanceStep,
DIRECTION_NONE, hasFollowPath, startAutoWalk and onFollowCreatureComplete to
ensure being exactly at targetDistance is treated as a completed follow.

@ranisalt ranisalt merged commit 809fb57 into dev Mar 2, 2026
10 checks passed
@ranisalt ranisalt deleted the fix/monster-spiral-movement branch March 2, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants