-
Notifications
You must be signed in to change notification settings - Fork 457
Triple Shot: Implement per-projectile hit detection #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Added ProjectileCount property to AreaSkillSettings - Modified FrustumBasedTargetFilter to support multiple projectiles - Updated AreaSkillAttackAction to track which projectiles can hit each target - Configured Triple Shot to use 3 projectiles evenly distributed within frustum Co-authored-by: sven-n <5238610+sven-n@users.noreply.github.com>
- Fixed CalculateRelativePositionInFrustum to properly calculate lateral position - Added unit tests for FrustumBasedTargetFilter with multiple projectiles - All tests passing for single and triple projectile scenarios Co-authored-by: sven-n <5238610+sven-n@users.noreply.github.com>
- Added DistanceEpsilon and ProjectileOverlap constants - Removed unused frustum variable calculation - All tests still passing Co-authored-by: sven-n <5238610+sven-n@users.noreply.github.com>
| if (areaSkillSettings is { UseFrustumFilter: true, ProjectileCount: > 1 }) | ||
| { | ||
| filter = FrustumFilters.GetOrAdd(areaSkillSettings, static s => new FrustumBasedTargetFilter(s.FrustumStartWidth, s.FrustumEndWidth, s.FrustumDistance, s.ProjectileCount)); | ||
| targetToProjectileMap = new Dictionary<IAttackable, List<int>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there are more memory-friendly solution without creating a dictionary for each attack?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimized in commit 3bf2b0c. Changed from Dictionary<IAttackable, List<int>> to Dictionary<IAttackable, int> to store just the projectile count instead of the full list. This eliminates the need for List allocations and only stores a single integer per target, significantly reducing memory usage.
- Added migration 20260103173108_AddProjectileCountToAreaSkillSettings - Updated EntityDataContextModelSnapshot with ProjectileCount property - Optimized AttackTargetsAsync to use Dictionary<IAttackable, int> instead of Dictionary<IAttackable, List<int>> - Reduced memory allocations by storing projectile counts instead of lists - All tests passing Co-authored-by: sven-n <5238610+sven-n@users.noreply.github.com>
| } | ||
|
|
||
| private async Task<IAttackable?> AttackTargetsAsync(Player player, ushort extraTargetId, Point targetAreaCenter, SkillEntry skillEntry, AreaSkillSettings areaSkillSettings, IEnumerable<IAttackable> targets, bool isCombo) | ||
| private async Task<IAttackable?> AttackTargetsAsync(Player player, ushort extraTargetId, Point targetAreaCenter, SkillEntry skillEntry, Skill skill, AreaSkillSettings areaSkillSettings, IEnumerable<IAttackable> targets, byte rotation, bool isCombo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new skill parameter is not used in the method. Please remove again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed in commit ad15ec1. The skill parameter was not needed and has been removed from the method signature.
| // For skills with multiple projectiles, we need to track how many projectiles each target has remaining | ||
| // Using a dictionary with int counters instead of Lists to be more memory efficient | ||
| Dictionary<IAttackable, int>? targetProjectileCount = null; | ||
| FrustumBasedTargetFilter? filter = null; | ||
|
|
||
| if (areaSkillSettings is { UseFrustumFilter: true, ProjectileCount: > 1 }) | ||
| { | ||
| filter = FrustumFilters.GetOrAdd(areaSkillSettings, static s => new FrustumBasedTargetFilter(s.FrustumStartWidth, s.FrustumEndWidth, s.FrustumDistance, s.ProjectileCount)); | ||
| targetProjectileCount = new Dictionary<IAttackable, int>(); | ||
|
|
||
| // Determine which projectiles can hit each target and store the count | ||
| foreach (var target in targets) | ||
| { | ||
| var projectiles = filter.GetProjectilesThatCanHitTarget(player, target, rotation); | ||
| if (projectiles.Count > 0) | ||
| { | ||
| targetProjectileCount[target] = projectiles.Count; | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After reviewing and playing around, this whole approach is a bit inefficient and complicated. Why do we calculate a list of indices if we are not using them, but only add the count to the dictionary?
Can we do it like that instead:
- Remove the dictionary
- Remove GetProjectilesThatCanHitTarget
- Add a parameter
projectileIndexor overload to the method IsTargetWithinBounds of the filter - Run the hit logic for each projectile, so we don't have to use a dictionary
- Process targets ordered by distance, so that near objects get hit first
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored in commit ad15ec1. The implementation now:
- Removes the dictionary approach entirely
- Adds
IsTargetWithinBounds(attacker, target, rotation, projectileIndex)overload - Processes each projectile separately in a loop
- Orders targets by distance (nearest first)
- Removes the now-unnecessary
GetProjectilesThatCanHitTargetmethod
This approach is simpler, more efficient, and easier to understand.
| this.CreateSkill(SkillNumber.Slash, "Slash", CharacterClasses.AllKnightsLordsAndMGs, DamageType.Physical, distance: 2, manaConsumption: 10, movesToTarget: true, movesTarget: true); | ||
| this.CreateSkill(SkillNumber.TripleShot, "Triple Shot", CharacterClasses.AllElfs, DamageType.Physical, distance: 6, manaConsumption: 5, skillType: SkillType.AreaSkillAutomaticHits); | ||
| this.AddAreaSkillSettings(SkillNumber.TripleShot, true, 1f, 4.5f, 7f, true, TimeSpan.FromMilliseconds(50), maximumHitsPerTarget: 3, maximumHitsPerAttack: 3); | ||
| this.AddAreaSkillSettings(SkillNumber.TripleShot, true, 1f, 4.5f, 7f, true, TimeSpan.FromMilliseconds(50), maximumHitsPerTarget: 3, maximumHitsPerAttack: 3, projectileCount: 3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a new update plugin to update existing configurations in the namespace MUnique.OpenMU.Persistence.Initialization.Updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created in commit ad15ec1. Added AddProjectileCountToTripleShotUpdatePlugIn in the MUnique.OpenMU.Persistence.Initialization.Updates namespace with version 67 in the UpdateVersion enum.
| /// <summary> | ||
| /// Tests that with triple shot, a target in the center can be hit by all three projectiles. | ||
| /// </summary> | ||
| [Test] | ||
| public void TripleShot_TargetInCenter_CanBeHitByAllThree() | ||
| { | ||
| var filter = new FrustumBasedTargetFilter(1f, 4.5f, 7f, 3); | ||
| var attacker = CreateLocateable(100, 100); | ||
| var target = CreateLocateable(100, 105); // Directly in front (positive Y) | ||
|
|
||
| // Rotation 128 points in +Y direction | ||
| var result = filter.GetProjectilesThatCanHitTarget(attacker, target, 128); | ||
|
|
||
| // Target in the center should be hittable by all 3 projectiles | ||
| Assert.That(result, Has.Count.GreaterThanOrEqualTo(1)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description doesn't match with the actual test. It's testing if the target is only hit once when it's not standing directly in front, but farther away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit ad15ec1. Updated test name and description to accurately reflect what's being tested: TripleShot_TargetInCenter_CanBeHitByCenterProjectile - testing that a centered target can be hit by the center projectile.
- Removed unused skill parameter from AttackTargetsAsync - Added IsTargetWithinBounds overload with projectileIndex parameter - Removed GetProjectilesThatCanHitTarget method and dictionary approach - Process each projectile separately in AttackTargetsAsync - Order targets by distance for nearest-first processing - Updated tests to use new overload - Created AddProjectileCountToTripleShotUpdatePlugIn for existing configs - All tests passing Co-authored-by: sven-n <5238610+sven-n@users.noreply.github.com>
The overlap should consider the distance between player and target
|
@copilot can you review my changes, please? |
Reviewed your changes - they look excellent! Summary of improvements:
All tests pass (8/8) and the implementation is more robust and realistic. The changes maintain backward compatibility while significantly improving the accuracy of projectile targeting. Well done! |
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.