Skip to content

DK Master Tree Finishup#766

Open
ze-dom wants to merge 10 commits intoMUnique:masterfrom
ze-dom:DK_master_tree_finishup
Open

DK Master Tree Finishup#766
ze-dom wants to merge 10 commits intoMUnique:masterfrom
ze-dom:DK_master_tree_finishup

Conversation

@ze-dom
Copy link
Copy Markdown
Contributor

@ze-dom ze-dom commented May 2, 2026

To-do

  • Comment changes with source references
  • UpdatePlugin

Developments

  • Added Cold magic effect number (similar to Iced; used in Chain Drive and Strike of Destruction skills)
  • Added Swell Life magic effect maximum values
  • Added Swell Life Proficiency magic effect
  • Added Twisting Slash Mastery, Rageful Blow Mastery, Spear Mastery, Mace Mastery, Swell Life Strengthener, and Swell Life Proficiency skill effects
  • Replaced Stats.PollutionMoveTargetChance and Stats.StunChance for the more generic and descriptive Stats.MasteryMoveTargetChance and Stats.MasteryStunChance, which can serve several classes

Bugfixes

  • Map names in logs
  • Changed Chain Drive magic effect number from Iced to Cold
  • Fixed Life Swell magic effect boost calcs
  • Fixed MagicEffect comparison in SkillsInitizalizerBase.CreateEffect() (bug introduced in previous PR)
  • Fixed double wield damage calcs (averaged the weapons DMG + item options and excellent options)

Comment on lines -373 to 383
var baseSkill = skillEntry.GetBaseSkill();

if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillPlugIn>(baseSkill.Number) is { } strategy)
if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillPlugIn>(skillEntry.Skill.Number) is { } strategy)
{
await strategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter, hitInfo).ConfigureAwait(false);
return;
}

var baseSkill = skillEntry.GetBaseSkill();
if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillPlugIn>(baseSkill.Number) is { } baseSkillStrategy)
{
await baseSkillStrategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter, hitInfo).ConfigureAwait(false);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added for TwistingSlashMasterySkillPlugIn.cs.
Another option is to change the Key there to the base Twisting Slash skill and keep this as before.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

var magicEffect = new MagicEffect(TimeSpan.FromSeconds(2), stunEffectDefinition, [new MagicEffect.ElementWithTarget(powerUp, Stats.IsStunned)]);
await attackable.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

{
return item.ItemSlot >= InventoryConstants.HelmSlot && item.ItemSlot <= InventoryConstants.BootsSlot;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

result.AttributeCombinations.Add(this.CreateAttributeRelationship(Stats.ComboBonus, 0.5f, Stats.TotalEnergy));

result.AttributeCombinations.Add(this.CreateConditionalRelationship(Stats.AttackSpeedAny, Stats.IsOneHandedSwordEquipped, Stats.WeaponMasteryAttackSpeed));
result.AttributeCombinations.Add(this.CreateConditionalRelationship(Stats.DoubleDamageChance, Stats.IsSpearEquipped, Stats.SpearMasteryDoubleDamageChance));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

magicEffect.StopByDeath = true;
magicEffect.Duration = this.Context.CreateNew<PowerUpDefinitionValue>();
magicEffect.Duration.ConstantValue.Value = 60f;
magicEffect.Duration.MaximumValue = 180f;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

emu

var boostPerPartyMember = this.Context.CreateNew<AttributeRelationship>();
boostPerPartyMember.InputAttribute = Stats.NearbyPartyMemberCount.GetPersistent(this.GameConfiguration);
boostPerPartyMember.InputOperator = InputOperator.Multiply;
boostPerPartyMember.InputOperand = 1f / 100;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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


healthPowerUpDefinition.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
healthPowerUpDefinition.Boost.ConstantValue.Value = 0.12f;
healthPowerUpDefinition.Boost.MaximumValue = 2f;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

emu

Comment on lines +71 to +81
// one percent per party member in view
var boostPerPartyMember = this.Context.CreateNew<AttributeRelationship>();
boostPerPartyMember.InputAttribute = Stats.NearbyPartyMemberCount.GetPersistent(this.GameConfiguration);
boostPerPartyMember.InputOperator = InputOperator.Multiply;
boostPerPartyMember.InputOperand = 1f / 100;

var manaPowerUpDefinition = this.Context.CreateNew<PowerUpDefinition>();
magicEffect.PowerUpDefinitions.Add(manaPowerUpDefinition);
manaPowerUpDefinition.TargetAttribute = Stats.SwellLifeManaIncrease.GetPersistent(this.GameConfiguration);
manaPowerUpDefinition.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
manaPowerUpDefinition.Boost.RelatedValues.Add(boostPerPartyMember);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

skill.SkipElementalModifier = true;
skill.MagicEffectDef = this.CreateEffect(ElementalType.Ice, MagicEffectNumber.Cold, Stats.IsIced, 10);
return;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

{
skill.ElementalModifierTarget = Stats.IceResistance.GetPersistent(this.GameConfiguration);
skill.SkipElementalModifier = true;
skill.MagicEffectDef = this.CreateEffect(ElementalType.Ice, MagicEffectNumber.Cold, Stats.IsIced, 10);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

/// The cold effect caused by chain drive and strike of destruction skills. Like <see cref="Iced"/>, it also slows down movement.
/// </summary>
Cold = 0x56,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Image Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines -71 to -74
if (multiValues == 0 && this.Elements.All(e => e.AggregateType != AggregateType.Multiplicate))
{
multiValues = 1;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is an impossible situation since in line 66 an enumerable with "1" is concatenated.

Comment thread src/GameLogic/Player.cs
{
await this.InvokeViewPlugInAsync<IItemDurabilityChangedPlugIn>(p => p.ItemDurabilityChangedAsync(randomArmorItem, false)).ConfigureAwait(false);
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

zTeamS6.3, emu
The sources seem to be bugged here, because they use the attacker's instead of the defender's durability reduction master skill to attenuate the effect.

@ze-dom ze-dom marked this pull request as ready for review May 8, 2026 14:54
@ze-dom
Copy link
Copy Markdown
Contributor Author

ze-dom commented May 8, 2026

All set!

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request completes the Dark Knight master tree implementation, introducing new skill effects and proficiency bonuses. It also refactors the attribute system to use more generic mastery-based stats and addresses critical bugs in double-wield damage calculations and magic effect handling.

Highlights

  • Master Tree Skills: Added and updated several master tree skills and effects for the Dark Knight class, including Twisting Slash Mastery and Mace Mastery.
  • Attribute System Refactor: Introduced generic MasteryMoveTargetChance and MasteryStunChance attributes to provide a scalable bucket for mastery-based skill effects across multiple classes.
  • Double Wield Fixes: Corrected double wield damage calculations by averaging weapon damage and properly adjusting item options to prevent over-scaling.
  • Magic Effects: Added the 'Cold' magic effect and updated 'Swell Life' proficiency and scaling to improve class balance.
  • Logging Improvements: Enhanced system logs by including specific map names, facilitating easier debugging and monitoring.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request completes the Dark Knight master tree implementation and refines double-wield damage calculations. Key changes include the introduction of new attributes for mastery skills, logic for mace-based stuns and durability reduction, and the ability to override attribute aggregation types for specific item options. Additionally, it includes several data initialization updates and a new plugin for Twisting Slash Mastery. The review feedback identifies several critical issues, including potential NullReferenceExceptions when accessing entity attributes, a logic regression in attribute defaulting, thread-safety concerns regarding shared static fields, and idempotency risks in database update scripts.

Comment on lines +71 to 74
if (this.Elements.All(e => e.AggregateType == AggregateType.Multiplicate))
{
rawValues = 1;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The new logic for defaulting rawValues to 1 is more restrictive than the previous implementation. By checking if all elements are of type Multiplicate, it fails to handle scenarios where the attribute contains other non-raw elements (like AddFinal or Maximum) but no AddRaw elements. In Mu Online's attribute system, multipliers should generally apply to a base of 1 if no explicit raw additions are present. The previous check this.Elements.All(e => e.AggregateType != AggregateType.AddRaw) correctly identified the absence of a base value regardless of other non-raw elements.

        if (this.Elements.All(e => e.AggregateType != AggregateType.AddRaw))
        {
            rawValues = 1;
        }

Copy link
Copy Markdown
Contributor Author

@ze-dom ze-dom May 8, 2026

Choose a reason for hiding this comment

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

That would fail if we have only AggregateType.AddFinal values. Suppose we only have a +3 AggregateType.AddFinal. Then, we get: 1*1 + 3 = 4, instead of 3.

In case the attribute has different values, like AggregatyeType.AddFinal and AggregateType.Multiplicate values, I think it makes more sense to explicitly add a AggregateType.AddRaw value of 1 (this seems to be the standard anyway), otherwise it becomes too messy with assumptions.

Comment thread src/GameLogic/Player.cs

await this.HitAsync(hitInfo, attacker, skill?.Skill, isFinalStreakHit).ConfigureAwait(false);
await this.DecreaseItemDurabilityAfterHitAsync(hitInfo).ConfigureAwait(false);
await this.DecreaseItemDurabilityAfterHitAsync(hitInfo, attacker.Attributes[Stats.RagefulBlowMasteryDurabilityDecChance]).ConfigureAwait(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Directly accessing attacker.Attributes is unsafe here. If the attacker is an entity without initialized attributes (which can happen with certain NPCs or in specific edge cases), this will throw a NullReferenceException. It's safer to use a null-conditional access with a default value.

        await this.DecreaseItemDurabilityAfterHitAsync(hitInfo, attacker.Attributes?[Stats.RagefulBlowMasteryDurabilityDecChance] ?? 0).ConfigureAwait(false);

e => e.Number == (short)effectNumber
&& e.SubType == (byte)(0xFF - type)
&& Equals(e.Duration?.ConstantValue.Value, durationInSeconds)
&& e.Chance?.ConstantValue.Value == chance
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

This comparison will throw a NullReferenceException if e.Chance is null, which is the case for many magic effects. Since the chance parameter defaults to 0, the check should safely handle null Chance properties by treating them as 0.

                     && (e.Chance?.ConstantValue.Value ?? 0) == chance

{ Stats.CurrentAbility, Stats.AbilityUsageReduction },
};

private static MagicEffectDefinition? stunEffectDefinition;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The static field stunEffectDefinition is shared across all instances and is not initialized in a thread-safe manner. In environments with multiple GameContext instances (e.g., multi-tenant servers or unit tests), this could lead to cross-context contamination if different configurations are used. Additionally, the lazy initialization ??= is subject to race conditions. Consider retrieving the definition from the attacker.GameContext directly within the method or using a thread-safe caching mechanism if performance is a concern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That seems to make sense. Remove static, @sven-n ?

this._stunEffectDefinition ??= ((Player)attacker).GameContext.Configuration.MagicEffects.First(m => m.Number == StunnedMagicEffectNumber);

if (!target.IsAlive || !Rand.NextRandomBool(Convert.ToDouble(attacker.Attributes[Stats.StunChance])))
if (!target.IsAlive || !Rand.NextRandomBool(attacker.Attributes[Stats.MasteryStunChance]))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Unsafe access to attacker.Attributes. If the attacker is not a player or has no attributes, this will throw a NullReferenceException. Use null-conditional access.

        if (!target.IsAlive || !Rand.NextRandomBool(attacker.Attributes?[Stats.MasteryStunChance] ?? 0))

if (!target.IsAlive
|| target is not IMovable movableTarget
|| target.CurrentMap is not { } currentMap
|| !Rand.NextRandomBool(attacker.Attributes[Stats.MasteryMoveTargetChance]))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Unsafe access to attacker.Attributes. If the attacker is not a player or has no attributes, this will throw a NullReferenceException. Use null-conditional access.

            || !Rand.NextRandomBool(attacker.Attributes?[Stats.MasteryMoveTargetChance] ?? 0))

twistingSlashMastery.ReplacedSkill = gameConfiguration.Skills.First(s => s.Number == (short)SkillNumber.TwistingSlashStreng);
twistingSlashMastery.TargetAttribute = Stats.MasteryMoveTargetChance.GetPersistent(gameConfiguration);
twistingSlashMastery.Aggregation = AggregateType.AddRaw;
twistingSlashMastery.ValueFormula = $"{twistingSlashMastery.ValueFormula} / 100";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The update to ValueFormula is not idempotent. If this update plugin is executed multiple times (e.g., during a failed migration retry), the formula will be appended with / 100 repeatedly (e.g., ... / 100 / 100), leading to incorrect skill values. You should check if the formula already contains the division or set the formula to a fixed string.

Comment on lines +277 to +294
if (option.OptionType == ItemOptionTypes.Excellent)
{
if (item.ItemSlot == InventoryConstants.PendantSlot
&& option.PowerUpDefinition?.TargetAttribute == Stats.PhysicalBaseDmg)
{
// Pendant options are not subject to double wield averaging
aggregateType = AggregateType.AddFinal;
}
else if (option.PowerUpDefinition?.TargetAttribute == Stats.PhysicalBaseDmgIncrease
&& item.Definition!.BasePowerUpAttributes.Any(pu => pu.TargetAttribute == Stats.DoubleWieldWeaponCount))
{
// This needs special treatment, since this option is averaged when double wielding
aggregateType = AggregateType.AddRaw;
}
else
{
// the normal aggregate type should be used
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Excellent weapon options should be averaged, as they only apply to their "side" in the sources, whereas the pendant excellent options add to both sides: zTeamS6.3, emu.
I used this aggregateType override to get that right 😃

attributeRelationships.Add(this.CreateAttributeRelationship(Stats.MaximumPhysBaseDmgByWeapon, 0.5f, Stats.HasDoubleWield, InputOperator.ExponentiateByAttribute, AggregateType.Multiplicate));
attributeRelationships.Add(this.CreateAttributeRelationship(Stats.PhysicalBaseDmg, 0.5f, Stats.HasDoubleWield, InputOperator.ExponentiateByAttribute, AggregateType.Multiplicate));
attributeRelationships.Add(this.CreateAttributeRelationship(Stats.PhysicalBaseDmgIncrease, 0.5f, Stats.HasDoubleWield, InputOperator.ExponentiateByAttribute, AggregateType.Multiplicate));
attributeRelationships.Add(this.CreateAttributeRelationship(Stats.PhysicalBaseDmgIncrease, 1, Stats.HasDoubleWield));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refer to this comment.

For example, if you double wield with weapons w/ item option (or exc lvl/20 option) (Stats.PhysicalBaseDmg):

  • 1 weapon with +12 dmg => +6 dmg
  • 2 weapons with +12 dmg => +12 dmg
  • 1 weapon with +8 dmg, 1 weapon with +4 dmg => +6 dmg

if you double wield with exc +2% dmg option (Stats.PhysicalBaseDmgIncrease):

  • 1 weapon with +2% dmg => +1% dmg
  • 2 weapons with +2% dmg => +2% dmg

This checks out with the client calculations in 'C' menu.

public override string? ToString()
{
return $"{this._parentAttribute?.Definition.Designation}: {this._element}";
return $"{this._parentAttribute?.Definition.Designation}: {this._element}{(this._aggregateType is { } aggreg ? $"->({aggreg})" : string.Empty)}";
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So that one can see that this aggregate type has been overriden.
E.g.: {Physical Base Damage: 20 (AddRaw)->(AddFinal)}

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.

1 participant