Skip to content

[Cursor Review] Implement piercing enchantment#30

Open
HashimTheArab wants to merge 13 commits into
HashimTheArab:masterfrom
mmm545:feature/piercing
Open

[Cursor Review] Implement piercing enchantment#30
HashimTheArab wants to merge 13 commits into
HashimTheArab:masterfrom
mmm545:feature/piercing

Conversation

@HashimTheArab

@HashimTheArab HashimTheArab commented May 16, 2026

Copy link
Copy Markdown
Owner

Cursor review mirror of df-mc#1246

Original: df-mc#1246


Note

Medium Risk
Modifies core projectile collision/damage flow and enchantment application rules, which can affect combat balance and entity interactions. Risk is moderate due to new state tracking for pierced entities and API signature changes for arrow spawning.

Overview
Adds the crossbow Piercing enchantment (registered as ID 34) and makes it mutually incompatible with Multishot.

Updates projectile handling so arrows can pass through multiple entities based on a new PiercingLevel, tracking already-hit entities to avoid repeat hits and only closing the projectile after exceeding the allowed pierces.

Refactors arrow spawning to use a single world.ArrowSpawnConfig struct (replacing the long parameter list) and threads this through EntityRegistryConfig, entity/register.go, Bow, and Crossbow (including reading the piercing enchant level when firing). Also tightens Stack.WithEnchantments to enforce both-direction enchantment compatibility against existing enchants.

Reviewed by Cursor Bugbot for commit 6960176. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added Piercing enchantment for crossbows (levels I–IV); arrows can pierce multiple targets up to the enchantment level
    • Projectiles now track hits to allow piercing and only stop when piercing capacity is exceeded
  • Balance & Improvements

    • Multishot and Piercing are now incompatible
    • Enchantment application now prevents conflicting enchantment combinations automatically
    • Arrow firing now consistently respects piercing, pickup, and related shot properties visible to players

Review Change Stack

@HashimTheArab

Copy link
Copy Markdown
Owner Author

@cursor review

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a Piercing enchantment and pierce-capable projectile behavior, refactors arrow spawn parameters into an exported ArrowSpawnConfig used by the entity registry and weapons, and enforces mutual enchantment compatibility when applying enchantments to item stacks.

Changes

Arrow Piercing Enchantment and Mechanics

Layer / File(s) Summary
Arrow spawn configuration contract
server/world/entity.go, server/entity/register.go
New exported ArrowSpawnConfig centralizes arrow spawn fields; EntityRegistryConfig.Arrow and the registry factory now accept this config and extract spawn properties including PiercingLevel.
Piercing enchantment definition and compatibility
server/item/enchantment/piercing.go, server/item/enchantment/multishot.go, server/item/enchantment/register.go
Adds Piercing enchantment (max level 4), restricts it to crossbows, marks it incompatible with Multishot, and registers it (ID 34).
Projectile piercing mechanics and collision tracking
server/entity/projectile.go
Adds PiercingLevel to ProjectileBehaviourConfig, tracks collidedEntities, applies damage conditionally, preserves velocity for piercing projectiles, gates closure by pierced count, and updates collision ignore rules.
Entity registry arrow factory
server/entity/register.go
Arrow factory refactored to accept world.ArrowSpawnConfig, extracts tip/potion, knockback addend, pickup flags, critical, and PiercingLevel when creating arrow entities.
Arrow spawning in bow and crossbow weapons
server/item/bow.go, server/item/crossbow.go
Bow and Crossbow build and pass ArrowSpawnConfig; Crossbow extracts piercing and multishot behavior, sets tip before spawning, and adjusts pickup behavior for multishot extras.
Bidirectional enchantment stack validation
server/item/stack.go
WithEnchantments now checks mutual compatibility between the new enchantment and existing enchantments and skips applying incompatible combinations.

Sequence Diagram(s)

sequenceDiagram
  participant Projectile as ProjectileBehaviour
  participant Trace as CollisionTrace
  participant Entity as TargetEntity
  participant HitCB as HitCallback
  participant Closure as ClosureCheck
  Projectile->>Trace: tickMovement with ignores filter
  Trace-->>Entity: detect entity collision
  Entity->>Projectile: append to collidedEntities
  Projectile->>Entity: apply Damage (if Damage >= 0)
  Projectile->>HitCB: invoke Hit callback (if set)
  HitCB->>Closure: return to projectile tick
  alt len(collidedEntities) > PiercingLevel
    Closure->>Projectile: Close projectile
  else
    Closure->>Projectile: Keep velocity / continue flying
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hop and watch the arrows fly,
Piercing ranks beneath the sky,
Configs in order, enchantments neat,
Code and carrot make my day complete! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: implementing the piercing enchantment, which is the core feature across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
server/entity/projectile.go (1)

315-334: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't discard the rest of the tick after a pierced hit.

When PiercingLevel > 0, the trace still stops at the first entity and end is truncated to hit.Position(), but the remaining segment is never processed. That makes piercing arrows lose travel distance on every hit and can skip a second entity or block that was still on the same tick path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/projectile.go` around lines 315 - 334, The trace currently
stops and truncates end to hit.Position() even when lt.conf.PiercingLevel > 0,
causing pierced projectiles to lose remaining travel and skip subsequent
collisions; update the logic in the trace block (where trace.Perform is called
and hit/end/vel are handled) to, when hit is an entity and lt.conf.PiercingLevel
> 0, continue processing the remaining segment from hit.Position() to the
original intended end (or loop repeatedly) instead of immediately truncating and
returning: perform additional trace.Perform calls on the remaining ray(s)
(starting from hit.Position() plus a small epsilon to avoid immediate re-hit),
decrement or account for piercing consumption per hit, accumulate the final end
and any subsequent hit(s) (or the last non-hit end) and only set the Movement
return values (pos: end, dpos, hit) after all segments are processed so pierced
hits do not lose travel distance; keep using the existing symbols pos, end, hit,
vel, trace.Perform, lt.conf.PiercingLevel and Movement to locate and implement
the looped/continued-trace fix.
🧹 Nitpick comments (1)
server/world/chunk/light.go (1)

91-107: ⚡ Quick win

Skip neighbour propagation once the light level reaches 1.

After a.setLight(...), n.level <= 1 cannot enqueue any neighbours because the minimum filter cost is already 1. Returning early here avoids six extra hot-path calls per terminal node.

♻️ Proposed change
 func (a *lightArea) propagate(queue *lightQueue) {
 	n, ok := queue.pop()
 	if !ok {
 		return
 	}
 	if a.light(n.pos, n.lt) >= n.level {
 		return
 	}
 	a.setLight(n.pos, n.lt, n.level)
+	if n.level <= 1 {
+		return
+	}
 
 	x, y, z := n.pos[0], n.pos[1], n.pos[2]
 	a.propagateNeighbour(queue, n.lt, n.level, x+1, y, z)
 	a.propagateNeighbour(queue, n.lt, n.level, x-1, y, z)
 	a.propagateNeighbour(queue, n.lt, n.level, x, y+1, z)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/world/chunk/light.go` around lines 91 - 107, In propagate (method
lightArea.propagate) after the call to a.setLight(n.pos, n.lt, n.level), check
if n.level <= 1 and return early to avoid calling a.propagateNeighbour six times
for terminal nodes; this prevents attempting to enqueue neighbours when the
minimum filter cost is 1 and eliminates the extra hot-path calls for those
nodes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/entity/register.go`:
- Around line 50-53: Make ArrowSpawnConfig handling zero-value safe in the Arrow
factory: guard the type assertion on arrow.Tip and the call to arrow.Owner.H()
so they don't panic when omitted—use a safe type-assertion for tip (e.g., tip,
ok := arrow.Tip.(potion.Potion); if !ok use zero-value potion.Potion{}) and
check arrow.Owner for nil (or presence of H) before calling H(), then assign
conf.Damage, conf.Potion and conf.Owner using those safe values in Arrow (the
Arrow function that sets conf from arrow and arrowConf).

---

Outside diff comments:
In `@server/entity/projectile.go`:
- Around line 315-334: The trace currently stops and truncates end to
hit.Position() even when lt.conf.PiercingLevel > 0, causing pierced projectiles
to lose remaining travel and skip subsequent collisions; update the logic in the
trace block (where trace.Perform is called and hit/end/vel are handled) to, when
hit is an entity and lt.conf.PiercingLevel > 0, continue processing the
remaining segment from hit.Position() to the original intended end (or loop
repeatedly) instead of immediately truncating and returning: perform additional
trace.Perform calls on the remaining ray(s) (starting from hit.Position() plus a
small epsilon to avoid immediate re-hit), decrement or account for piercing
consumption per hit, accumulate the final end and any subsequent hit(s) (or the
last non-hit end) and only set the Movement return values (pos: end, dpos, hit)
after all segments are processed so pierced hits do not lose travel distance;
keep using the existing symbols pos, end, hit, vel, trace.Perform,
lt.conf.PiercingLevel and Movement to locate and implement the
looped/continued-trace fix.

---

Nitpick comments:
In `@server/world/chunk/light.go`:
- Around line 91-107: In propagate (method lightArea.propagate) after the call
to a.setLight(n.pos, n.lt, n.level), check if n.level <= 1 and return early to
avoid calling a.propagateNeighbour six times for terminal nodes; this prevents
attempting to enqueue neighbours when the minimum filter cost is 1 and
eliminates the extra hot-path calls for those nodes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cf25738-003d-4d0c-ada9-f000c64785e2

📥 Commits

Reviewing files that changed from the base of the PR and between 7869803 and 43f3c01.

📒 Files selected for processing (10)
  • server/entity/projectile.go
  • server/entity/register.go
  • server/item/bow.go
  • server/item/crossbow.go
  • server/item/enchantment/multishot.go
  • server/item/enchantment/piercing.go
  • server/item/enchantment/register.go
  • server/world/chunk/light.go
  • server/world/chunk/light_area.go
  • server/world/entity.go

Comment thread server/entity/register.go
Comment on lines +50 to +53
Arrow: func(opts world.EntitySpawnOpts, arrow world.ArrowSpawnConfig) *world.EntityHandle {
tip := arrow.Tip.(potion.Potion)
conf := arrowConf
conf.Damage, conf.Potion, conf.Owner = damage, tip.(potion.Potion), owner.H()
conf.KnockBackForceAddend = float64(punchLevel) * enchantment.Punch.KnockBackMultiplier()
conf.DisablePickup = disallowPickup
if obtainArrowOnPickup {
conf.PickupItem = item.NewStack(item.Arrow{Tip: tip.(potion.Potion)}, 1)
conf.Damage, conf.Potion, conf.Owner = arrow.Damage, tip, arrow.Owner.H()

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 | 🟠 Major | ⚡ Quick win

Make ArrowSpawnConfig zero-value safe here.

arrow.Tip.(potion.Potion) and arrow.Owner.H() will panic if a caller omits the tip or spawns an ownerless arrow. The new struct API makes both omissions easy.

💡 Suggested fix
 Arrow: func(opts world.EntitySpawnOpts, arrow world.ArrowSpawnConfig) *world.EntityHandle {
-	tip := arrow.Tip.(potion.Potion)
+	var tip potion.Potion
+	if t, ok := arrow.Tip.(potion.Potion); ok {
+		tip = t
+	}
 	conf := arrowConf
-	conf.Damage, conf.Potion, conf.Owner = arrow.Damage, tip, arrow.Owner.H()
+	conf.Damage, conf.Potion = arrow.Damage, tip
+	if arrow.Owner != nil {
+		conf.Owner = arrow.Owner.H()
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Arrow: func(opts world.EntitySpawnOpts, arrow world.ArrowSpawnConfig) *world.EntityHandle {
tip := arrow.Tip.(potion.Potion)
conf := arrowConf
conf.Damage, conf.Potion, conf.Owner = damage, tip.(potion.Potion), owner.H()
conf.KnockBackForceAddend = float64(punchLevel) * enchantment.Punch.KnockBackMultiplier()
conf.DisablePickup = disallowPickup
if obtainArrowOnPickup {
conf.PickupItem = item.NewStack(item.Arrow{Tip: tip.(potion.Potion)}, 1)
conf.Damage, conf.Potion, conf.Owner = arrow.Damage, tip, arrow.Owner.H()
Arrow: func(opts world.EntitySpawnOpts, arrow world.ArrowSpawnConfig) *world.EntityHandle {
var tip potion.Potion
if t, ok := arrow.Tip.(potion.Potion); ok {
tip = t
}
conf := arrowConf
conf.Damage, conf.Potion = arrow.Damage, tip
if arrow.Owner != nil {
conf.Owner = arrow.Owner.H()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/register.go` around lines 50 - 53, Make ArrowSpawnConfig
handling zero-value safe in the Arrow factory: guard the type assertion on
arrow.Tip and the call to arrow.Owner.H() so they don't panic when omitted—use a
safe type-assertion for tip (e.g., tip, ok := arrow.Tip.(potion.Potion); if !ok
use zero-value potion.Potion{}) and check arrow.Owner for nil (or presence of H)
before calling H(), then assign conf.Damage, conf.Potion and conf.Owner using
those safe values in Arrow (the Arrow function that sets conf from arrow and
arrowConf).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0cf3d5a. Configure here.

Comment thread server/item/stack.go
if !compatible {
// Enchantment is not compatible with another enchantment on the item.
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WithItem silently drops incompatible enchantments due to new check

Medium Severity

The new enchantment compatibility check in WithEnchantments causes WithItem (line 374) to silently drop incompatible enchantments when copying them. WithItem calls WithEnchantments(s.Enchantments()...), and since Enchantments() iterates a map in non-deterministic order, which of two incompatible enchantments gets dropped is random. Items loaded from NBT via WithForcedEnchantments can legitimately hold incompatible enchantments; those are now randomly pruned whenever WithItem is called — for example in Crossbow.ReleaseCharge. The copy in WithItem likely needs to use WithForcedEnchantments to preserve existing enchantments faithfully.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0cf3d5a. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
server/entity/projectile.go (1)

179-183: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep side effects for harmless projectiles.

Line 180 now skips hitEntity() whenever Damage < 0, which also drops potion/fire application. ProjectileBehaviourConfig.Damage only promises “not hurt” / “not knocked back”, so this broadens the contract for any projectile that is meant to be harmless but still apply on-hit effects. Call hitEntity() unconditionally here and gate only the damage/knockback path inside hitEntity().

Suggested direction
-			if lt.conf.Damage >= 0 {
-				lt.hitEntity(l, e, vel)
-			}
+			lt.hitEntity(l, e, vel)
// Then, inside hitEntity(), guard Hurt/KnockBack on lt.conf.Damage >= 0,
// but keep the potion/fire application path intact.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/projectile.go` around lines 179 - 183, The current code skips
lt.hitEntity(l, e, vel) whenever lt.conf.Damage < 0, which prevents non-damaging
projectiles from applying potion/fire effects; instead call lt.hitEntity(...)
unconditionally for Living targets and move the Damage >= 0 guard into hitEntity
so only the Hurt/KnockBack paths are gated by lt.conf.Damage, while leaving
potion/fire application and other side-effects intact; update hitEntity (and any
helpers it calls) to check lt.conf.Damage >= 0 before performing
damage/knockback but always execute effect-application logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@server/entity/projectile.go`:
- Around line 179-183: The current code skips lt.hitEntity(l, e, vel) whenever
lt.conf.Damage < 0, which prevents non-damaging projectiles from applying
potion/fire effects; instead call lt.hitEntity(...) unconditionally for Living
targets and move the Damage >= 0 guard into hitEntity so only the Hurt/KnockBack
paths are gated by lt.conf.Damage, while leaving potion/fire application and
other side-effects intact; update hitEntity (and any helpers it calls) to check
lt.conf.Damage >= 0 before performing damage/knockback but always execute
effect-application logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06adb743-4d27-4eaf-ab69-0b3e6c913158

📥 Commits

Reviewing files that changed from the base of the PR and between 40a93c7 and 6960176.

📒 Files selected for processing (2)
  • server/entity/projectile.go
  • server/world/entity.go

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