Skip to content

Fix NPC skill hitbox detection and add debug tooling#646

Merged
AngeloTadeucci merged 10 commits intomasterfrom
dev
Feb 18, 2026
Merged

Fix NPC skill hitbox detection and add debug tooling#646
AngeloTadeucci merged 10 commits intomasterfrom
dev

Conversation

@AngeloTadeucci
Copy link
Collaborator

@AngeloTadeucci AngeloTadeucci commented Feb 17, 2026

Summary

  • Fix SAT (Separating Axis Theorem) intersection bug where Circle vs Polygon checks skipped polygon edge normals, causing missed collisions
  • Fix NPC skill hitbox direction by applying +180 rotation to align with NPC forward vector and use forward-projected Trapezoid for Box range type
  • Fix server-side NPC skill target resolution in SkillState to use prism-based hit detection
  • Fix knockback direction using ImpactPosition fallback in Actor
  • Fix SkillMapper ingesting height instead of width for skill range Width (all skill widths in DB were wrong)
  • Fix safe revive blocked when RevivalReturnId is 0
  • Add mob-attack command and --no-ai flag for NPC spawn to test skill hitboxes
  • Add skill hitbox visualization to DebugGame with toggle in visualization controls
  • Add collision unit tests for Polygon intersection and Prism intersection

Summary by CodeRabbit

  • New Features

    • Skill hitbox visualization in debug mode with UI toggle control
    • Mob attack debug command for skill casting control
    • NPC spawn command enhanced with --no-ai option and randomized spawn positions
  • Bug Fixes

    • Improved damage position calculation for skill impacts
    • Revival condition logic refinement
  • Tests

    • Added collision detection tests for polygons, circles, and 3D prism intersections

Polygon.Intersects() was only testing the polygon's own edge normals
when the other shape was also a Polygon. When intersecting with a
Circle, those axes were never tested, causing all Circle vs Polygon
checks to incorrectly return true regardless of position.
- GetPrism: use Trapezoid instead of Rectangle for Box type so the
  hitbox projects forward from the caster instead of being centered.
  Apply RotateZDegree, RangeOffset, and +180 to match the NPC's
  facing convention
- SkillState: always resolve targets using the attack range prism
  rather than falling back to the client-provided target list
- Actor.TargetAttack: fall back to caster position when ImpactPosition
  is unset (NPC casts never set it from a client packet)
Allows spawning NPCs without AI so they stand still, useful for
testing skill hitboxes without the NPC moving or attacking.
Allows forcing a spawned NPC to cast a specific skill at a chosen
attack point, making it easier to test individual hitboxes in isolation.
Renders the current attack point's prism wireframe in orange for all
actors with active skills. Toggleable via the Visualization Controls
window. NPC skills are sourced from MovementState.CastTask; player
skills from ActiveSkills when in a Skill animation.

Supporting changes:
- Pass skill metadata to TryPlaySequence so AnimationRecord tracks
  which skill is active for NPCs
- Add SkillQueue.GetMostRecent() for player skill lookup
The NoRevivalHere check was also gating on RevivalReturnId == 0,
preventing revival in maps that have no return point configured
but should still allow in-place revive.
SkillMetadataRange.Width was incorrectly set to region.height,
causing every skill's hitbox width to use the height value instead.
Requires re-ingesting skill data.
Scale circle projection by axis length to fix SAT calculations: Circle.AxisProjection now multiplies Radius by axis.Length().

Add comprehensive collision unit tests: PolygonIntersectTests (polygon vs polygon/circle SAT cases) and PrismTests (3D prism height+polygon overlap). Adjust existing CircleTests and HoleCircleTest coordinates to match corrected geometry expectations.

Minor code/cleanup: simplify Prism type reference in SkillState and update how the attack prism is passed to GetTargets; clarify Box behavior comment in SkillUtils.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Warning

Rate limit exceeded

@AngeloTadeucci has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR extends skill mechanics with debug visualization, adds commands for mob attacks and AI-controlled NPC spawning, refines skill target resolution to consistently use spatial queries, improves skill metadata geometry calculations, and expands collision detection testing for prism-based intersections.

Changes

Cohort / File(s) Summary
Skill Metadata & Mapping
Maple2.File.Ingest/Mapper/SkillMapper.cs
Changed SkillMetadataRange.Width mapping to derive from region.width instead of region.height.
Debug Visualization
Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs, Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs
Added skill hitbox rendering pipeline with ShowSkillHitboxes toggle. Includes RenderSkillHitboxes, RenderActorSkillHitbox, and RenderPrism methods to render active skill hitboxes as 3D primitives with polygon type support. UI checkbox wired to toggle feature.
NPC & Mob Commands
Maple2.Server.Game/Commands/MobAttackCommand.cs, Maple2.Server.Game/Commands/NpcCommand.cs, Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs
Introduced MobAttackCommand for commanding mob skill attacks with optional skill ID and required level. Extended NpcCommand with --no-ai flag to spawn NPCs without AI. Updated SpawnNpc signature to accept disableAi parameter and added randomized spawn position with navmesh snapping for SpawnPointNPC.
Skill Mechanics & Casting
Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs, Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs, Maple2.Server.Game/Model/Field/Actor/Actor.cs, Maple2.Server.Game/Model/Skill/SkillQueue.cs
Refactored SkillState to always resolve targets via attack range prism instead of conditional fallback. Added skill metadata parameter to TryPlaySequence call in SkillCastTask. Implemented fallback for ImpactPosition using caster's position when record.ImpactPosition is zero. Added GetMostRecent() method to SkillQueue.
Geometry & Utilities
Maple2.Server.Game/Util/SkillUtils.cs, Maple2.Tools/Collision/Circle.cs, Maple2.Tools/Collision/Polygon.cs
Adjusted prism geometry construction to incorporate RangeOffset and adjustedAngle for all region types. Updated AxisProjection in Circle to scale by axis magnitude. Modified Polygon.Intersects to always perform SAT axis tests regardless of shape type, fixing Circle-vs-Polygon cases.
Game Flow
Maple2.Server.Game/PacketHandlers/RevivalHandler.cs
Removed RevivalReturnId != 0 check from safe revival condition; now only checks NoRevivalHere flag.
Collision Testing
Maple2.Server.Tests/Tools/Collision/CircleTests.cs, Maple2.Server.Tests/Tools/Collision/HoleCircleTest.cs, Maple2.Server.Tests/Tools/Collision/PolygonIntersectTests.cs, Maple2.Server.Tests/Tools/Collision/PrismTests.cs
Updated existing test cases with adjusted positions to reflect expected behavior changes. Added comprehensive PolygonIntersectTests suite for Trapezoid-Circle intersections and rotated configurations. Introduced PrismTests suite for 3D collision detection with height-range validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

Skill / Buff

Suggested reviewers

  • Zintixx
  • mettaursp

Poem

🐰 Whiskers twitch with debugging delight,
Prisms dance in debug light so bright,
Skills now snap to spatial truth,
Mobs attack with newfound youth,
Geometry refined, collisions fixed just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% 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 clearly and accurately summarizes the main changes: fixing NPC skill hitbox detection and adding debug tooling for testing and visualization.

✏️ 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 dev

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
Contributor

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs (1)

70-86: ⚠️ Potential issue | 🟠 Major

Fix redundant target limit handling and ensure minimum query limit.

When attack.TargetCount is 0 or negative, GetTargets at line 73 passes this value as limit to Filter in SkillUtils.cs. The Filter method immediately yields break when limit <= 0, returning no targets. However, lines 78–79 contain fallback logic that attempts to handle TargetCount == 0 by using 1 instead — this code is unreachable when TargetCount is 0 because resolvedTargets is empty.

Calculate the minimum target limit before querying:

Proposed fix
-        Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z);
+        int targetLimit = attack.TargetCount > 0 ? attack.TargetCount : 1;
+        Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z);
         var resolvedTargets = new List<IActor>();
-        foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range.ApplyTarget, attack.TargetCount)) {
+        foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range.ApplyTarget, targetLimit)) {
             resolvedTargets.Add(target);
         }
 
         if (resolvedTargets.Count > 0) {
-            int limit = attack.TargetCount > 0 ? attack.TargetCount : 1;
-            for (int i = 0; i < resolvedTargets.Count && i < limit; i++) {
+            for (int i = 0; i < resolvedTargets.Count && i < targetLimit; i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs`
around lines 70 - 86, The query is using attack.TargetCount directly which, when
<=0, causes SkillUtils.Filter to return no targets; compute a safe minimum limit
before calling actor.Field.GetTargets and pass that limit instead of
attack.TargetCount (e.g., int queryLimit = attack.TargetCount > 0 ?
attack.TargetCount : 1) so GetTargets returns at least one candidate; then keep
the existing loop that enforces the final send limit (int limit =
attack.TargetCount > 0 ? attack.TargetCount : 1) and call
actor.TargetAttack(cast) as before, referencing attack.TargetCount,
attack.Range.GetPrism, actor.Field.GetTargets, resolvedTargets,
cast.Targets.TryAdd, and actor.TargetAttack to locate the affected code.
🧹 Nitpick comments (3)
Maple2.Server.Game/Commands/MobAttackCommand.cs (1)

56-65: Consider validating that the mob actually owns the requested skill.

Line 58 validates that the skill exists in field metadata, but doesn't check whether the mob's Skill.Entries include this skillId. A user could pass a valid skill that the mob can't actually cast, which may produce confusing behavior. For a debug command this is arguably fine (and even useful for testing arbitrary skills), but a brief note in the output would help:

Optional: warn if skill isn't in mob's skill list
         // Validate skill exists in metadata
         if (!session.Field.SkillMetadata.TryGet(skillId.Value, skillLevel, out SkillMetadata? _)) {
             ctx.Console.Error.WriteLine($"Skill {skillId.Value} level {skillLevel} not found in metadata.");
             return;
         }
 
+        if (!entries.Any(e => e.Id == skillId.Value)) {
+            ctx.Console.Out.WriteLine($"Warning: Skill {skillId.Value} is not in this mob's skill list. Casting anyway.");
+        }
+
         // Cast the skill facing the player
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Maple2.Server.Game/Commands/MobAttackCommand.cs` around lines 56 - 65, Before
casting, check whether the target mob actually has the requested skill in its
own skill list (e.g., inspect mob.Skill.Entries or equivalent) after the
existing session.Field.SkillMetadata.TryGet validation; if the mob does not own
the skill, write a clear warning to ctx.Console.Out or ctx.Console.Error
(including mob.Value.Metadata.Name and the skillId/skillLevel) and then either
return or proceed based on intended debug behavior (for a debug command,
emitting the warning and continuing to call mob.CastAiSkill(skillId.Value,
skillLevel, faceTarget: 1, facePos: session.Player.Position) is acceptable).
Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs (2)

1292-1319: Trapezoid visualization uses an averaged-width box approximation.

This renders a uniform-width wireframe cube instead of a true trapezoid shape (which has different start/end widths). For debug visualization purposes this is reasonable, but worth noting the visual won't precisely match the actual collision geometry. A brief inline comment would help future maintainers understand this is intentional.

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

In `@Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs` around lines 1292 -
1319, The Trapezoid branch is using an averaged width (avgWidth = (range.Width +
range.EndWidth) * 0.5f) to render a uniform wireframe cube, which does not
reflect the true trapezoidal collision shape; update the Trapezoid handling in
DebugFieldRenderer (the block that computes center2D/center, avgWidth, distance,
height, rotation and sets instanceBuffer.Transformation, then calls
UpdateWireframeInstance and WireCube.Draw) to include a brief inline comment
stating that this is an intentional visual approximation for debug purposes
(uniform-width box using avgWidth) so future maintainers know the loss of
start/end width fidelity is expected and not a bug.

62-62: Consider defaulting ShowSkillHitboxes to false.

All other "advanced" visualization flags like ShowMeshColliders, ShowSpawnPoints, and ShowVibrateObjects default to false, while only foundational ones like ShowBoxColliders, ShowPortals, and ShowActors default to true. Skill hitboxes are transient and potentially noisy — defaulting to true may clutter the view for users who aren't actively debugging skills.

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

In `@Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs` at line 62, The field
ShowSkillHitboxes is defaulting to true, which is inconsistent with other
advanced visualization flags; change its initializer to false so
ShowSkillHitboxes = false; and update the inline comment if needed to reflect
it’s off by default; ensure there are no other places that assume it starts true
(search for usages of ShowSkillHitboxes in DebugFieldRenderer and related
rendering code) and run tests/verify visually that skill hitboxes remain off
unless explicitly enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs`:
- Around line 1320-1332: The fallback uses a hardcoded center Vector3(0,0,...)
so the box always renders at world origin; update the fallback in
DebugFieldRenderer (the else branch that sets variable center and
instanceBuffer.Transformation) to compute the X/Y from the polygon or actor
instead of 0: derive center.X/center.Y from the polygon's bounding box or vertex
average (use IPolygon vertices/bounds if available) or accept and use the actor
position passed in from RenderActorSkillHitbox, then set
instanceBuffer.Transformation the same way and call
UpdateWireframeInstance(window) and WireCube.Draw() so the fallback box appears
at the correct location.

In `@Maple2.Server.Tests/Tools/Collision/PolygonIntersectTests.cs`:
- Around line 13-15: The class comment in PolygonIntersectTests.cs is stale
because Circle.AxisProjection now multiplies Radius by axis.Length(), making
edge-touching projections scale-consistent; update or remove the note that warns
about "barely-touching-edge cases" being unreliable. Locate the comment block
above the test class (mentions Circle.AxisProjection and Radius) and either
reword it to reflect that Circle projections are now scaled by axis.Length() and
comparable to polygon projections, or delete the caveat entirely so tests no
longer contain misleading guidance.

---

Outside diff comments:
In `@Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs`:
- Around line 70-86: The query is using attack.TargetCount directly which, when
<=0, causes SkillUtils.Filter to return no targets; compute a safe minimum limit
before calling actor.Field.GetTargets and pass that limit instead of
attack.TargetCount (e.g., int queryLimit = attack.TargetCount > 0 ?
attack.TargetCount : 1) so GetTargets returns at least one candidate; then keep
the existing loop that enforces the final send limit (int limit =
attack.TargetCount > 0 ? attack.TargetCount : 1) and call
actor.TargetAttack(cast) as before, referencing attack.TargetCount,
attack.Range.GetPrism, actor.Field.GetTargets, resolvedTargets,
cast.Targets.TryAdd, and actor.TargetAttack to locate the affected code.

---

Nitpick comments:
In `@Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs`:
- Around line 1292-1319: The Trapezoid branch is using an averaged width
(avgWidth = (range.Width + range.EndWidth) * 0.5f) to render a uniform wireframe
cube, which does not reflect the true trapezoidal collision shape; update the
Trapezoid handling in DebugFieldRenderer (the block that computes
center2D/center, avgWidth, distance, height, rotation and sets
instanceBuffer.Transformation, then calls UpdateWireframeInstance and
WireCube.Draw) to include a brief inline comment stating that this is an
intentional visual approximation for debug purposes (uniform-width box using
avgWidth) so future maintainers know the loss of start/end width fidelity is
expected and not a bug.
- Line 62: The field ShowSkillHitboxes is defaulting to true, which is
inconsistent with other advanced visualization flags; change its initializer to
false so ShowSkillHitboxes = false; and update the inline comment if needed to
reflect it’s off by default; ensure there are no other places that assume it
starts true (search for usages of ShowSkillHitboxes in DebugFieldRenderer and
related rendering code) and run tests/verify visually that skill hitboxes remain
off unless explicitly enabled.

In `@Maple2.Server.Game/Commands/MobAttackCommand.cs`:
- Around line 56-65: Before casting, check whether the target mob actually has
the requested skill in its own skill list (e.g., inspect mob.Skill.Entries or
equivalent) after the existing session.Field.SkillMetadata.TryGet validation; if
the mob does not own the skill, write a clear warning to ctx.Console.Out or
ctx.Console.Error (including mob.Value.Metadata.Name and the skillId/skillLevel)
and then either return or proceed based on intended debug behavior (for a debug
command, emitting the warning and continuing to call
mob.CastAiSkill(skillId.Value, skillLevel, faceTarget: 1, facePos:
session.Player.Position) is acceptable).

Change debug renderer defaults and visuals: disable skill hitbox rendering by default, add comments about visual approximation for trapezoids, and compute 2D center from polygon vertices for fallback prism rendering. Add a warning in the MobAttackCommand when a mob casts a skill not present in its skill list. Fix SkillState target resolution by ensuring the field query uses a minimum query limit (uses attack.TargetCount if >0, otherwise 1) when resolving targets. Also update a test comment to clarify how circle axis projections are scaled.
@AngeloTadeucci AngeloTadeucci merged commit 50af87c into master Feb 18, 2026
4 of 5 checks passed
@AngeloTadeucci AngeloTadeucci deleted the dev branch February 18, 2026 00:39
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

Comments