Skip to content

Item Durability Logic Refactor - #966

Open
ze-dom wants to merge 20 commits into
MUnique:masterfrom
ze-dom:item_durability_refactor
Open

ze-dom wants to merge 20 commits into
MUnique:masterfrom
ze-dom:item_durability_refactor

Conversation

@ze-dom

@ze-dom ze-dom commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

To-do

  • Comment changes with source references
  • UpdatePlugin(s)

Developments

  • Added durability thresholds, past which some of the item power-ups decrease by a certain factor
  • Added DurabilityDecayPlugIn.cs, for jewelry and wings durability decrease
  • Reworked durability reduction calculations to reflect original values while keeping it configurable

Bugfixes

  • Possible durability reduction target items after dealing and receiving damage
  • Master tree logic and values for skills DurabilityReduction1, DurabilityReduction2, DurabilityReduction3, PetDurabilityStr, DurabilityReduction1FistMaster, DurabilityReduction2FistMaster, DurabilityReduction3FistMaster
  • Repairing items now raises the event EquippedItemsChanged. Previously repaired items w/ durability = 0 were not removing their power-ups.

Comment thread src/GameLogic/Player.cs
Comment on lines +737 to +739
bool isPvpDamage = attacker is Player or IPlayerSurrogate;
float damage = isPvpDamage ? hitInfo.TotalDamage : attacker.Attributes[Stats.MinimumPhysBaseDmg];
await this.DecreaseItemDurabilityAfterHitAsync(damage, isPvpDamage, skill).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.

if (attacker is Player player)
{
await player.AfterHitTargetAsync().ConfigureAwait(false);
await player.AfterHitTargetAsync(this.Attributes[Stats.DefensePvm], skill?.Skill?.DamageType).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.

if (attacker as IPlayerSurrogate is { } playerSurrogate)
{
await playerSurrogate.Owner.AfterHitTargetAsync().ConfigureAwait(false);
await playerSurrogate.Owner.DecreaseRavenDurabilityAfterHitAsync(hitInfo.TotalDamage).ConfigureAwait(false);

@ze-dom ze-dom Sep 22, 2026 •

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

Unfortunately Dark Raven durability decrease is originally calculated in the middle of damage calcs, so we diverge here a bit.

this.AddRandomExcellentOptions(resultItem, player);

resultItem.Durability = craftingResultItem.Durability ?? resultItem.GetMaximumDurabilityOfOnePiece();

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.

Durability depends on item being excellent

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.

Moved to Player.cs

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.

{
double decrementWeight = new ItemIdentifier(item.Definition!.Number, item.Definition.Group) switch
{
var _ when isTransformationRing => 11.28, // 11.28 / 564 = 0.02

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.

durationIncrease = 1;
}

var decrement = decrementWeight / (player.GameContext.Configuration.HitsPerOneItemDurability * durationIncrease);

@ze-dom ze-dom Sep 22, 2026 •

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
564 hard limit check

/// </summary>
public static AttributeDefinition ItemDurationIncrease { get; } = new(new Guid("03EBE702-90FF-473C-8CBB-E83669FE3C68"), "Item Duration Increase", string.Empty);
/// <remarks>Includes weapons, shields and armor items. Excludes jewelry, wings and pets.</remarks>
public static AttributeDefinition WeaponAndArmorDurationIncrease { get; } = new(new Guid("03EBE702-90FF-473C-8CBB-E83669FE3C68"), "Weapon And Armor Duration Increase (MST)", string.Empty);

@ze-dom ze-dom Sep 23, 2026 •

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

Image

The tooltip is inaccurate: it's actually 67% increase: zTeamS6.3, emu.

/// Gets the jewelry and wings duration increase MST attribute definition.
/// </summary>
public static AttributeDefinition PetDurationIncrease { get; } = new(new Guid("B4455150-D3A9-4A5F-914B-F41F9387FE9A"), "Pet Duration Increase", string.Empty);
public static AttributeDefinition JewelryAndWingsDurationIncrease { get; } = new(new Guid("E539FDD2-72C5-484A-B0F4-40565ABAEDD9"), "Jewelry And Wings Duration Increase (MST)", string.Empty);

@ze-dom ze-dom Sep 23, 2026 •

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

Image

The tooltip is inaccurate: it's actually 67% increase: zTeamS6.3, emu.

/// Gets the pet duration increase MST attribute definition.
/// </summary>
/// <remarks>Applies to imp, guardian angel, uniria, dinorant and fenrir. Cash shop items excluded.</remarks>
public static AttributeDefinition PetDurationIncrease { get; } = new(new Guid("E9CCA156-E0DE-4113-A249-B7CD4C0F610D"), "Pet Duration Increase (MST)", string.Empty);

@ze-dom ze-dom Sep 23, 2026 •

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

Image

The tooltip is inaccurate: it's actually x66.78 increase: zTeamS6.3 (missing), emu.

/// <summary>
/// Gets the trainable pet (dark horse, dark raven) duration increase MST attribute definition.
/// </summary>
public static AttributeDefinition TrainablePetDurationIncrease { get; } = new(new Guid("B4455150-D3A9-4A5F-914B-F41F9387FE9A"), "Trainable Pet Duration Increase (MST)", string.Empty);

@ze-dom ze-dom Sep 23, 2026 •

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
Image
The tooltip is innacurate: it's not actually %, but absolute in this case:
zTeamS6.3: Horse, Raven
emu: Horse, Raven.

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.

Move this to Player.cs, where all durability reduction is addressed.

double decrementWeight = identifier switch
{
var _ when isTransformationRing => 11.28, // 11.28 / 564 = 0.02
var itm when itm == ItemConstants.WizardsRing => 7,

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 _ when isTransformationRing => 11.28, // 11.28 / 564 = 0.02
var itm when itm == ItemConstants.WizardsRing => 7,
var itm when itm == ItemConstants.MoonstonePendant => 6.3,

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
"Pendant" that goes on ring slot 😅

private static readonly byte[] AdditionalDurabilityPerLevel = { 0, 1, 2, 3, 4, 6, 8, 10, 12, 14, 17, 21, 26, 32, 39, 47 };

private static readonly float[] DurabilityFactors = { 0.2f, 0.3f, 0.5f };

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 +320 to +323
if (!(left?.IsWizardryWeapon(out _) ?? true) || (left?.CanHaveSkill() ?? false))
{
left = null;
}

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.

We need the second condition to clear MG "magic swords"

var maxDurability = item.GetMaximumDurabilityOfOnePiece();
for (int i = 0; i < DurabilityFactors.Length; i++)
{
item.DurabilityThresholds[i] = maxDurability * DurabilityFactors[i];

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.

{
if (item.Durability < item.DurabilityThresholds[i])
{
return 1 - DurabilityFactors[^(i + 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.

Stats.StaffRise,
Stats.ScepterRise,
Stats.BookRise,
];

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.


foreach (var wrapper in PowerUpWrapper.CreateByPowerUpDefinition(powerUp, attributeHolder, aggregateType))
var durabilityFactor = (option.OptionType == ItemOptionTypes.Option && !item.IsJewelry()) || option.OptionType == ItemOptionTypes.AncientBonus
? item.GetCurrentDurabilityFactor() : 1.0f;

@ze-dom ze-dom Sep 23, 2026 •

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: zzzitem.cpp (regular item options - except rings and wings hp rec), zzzitem.cpp (ancient bonus opt)
emu: zzzitem.cpp, zzzitem.cpp

Comment thread src/GameLogic/Player.cs
if (attacker is Player attackerPlayer)
{
await attackerPlayer.AfterHitTargetAsync().ConfigureAwait(false);
await attackerPlayer.AfterHitTargetAsync(this.Attributes[Stats.DefenseFinal], skill?.Skill?.DamageType).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.

Comment thread src/GameLogic/Player.cs
item.PetExperience = (int)Math.Max((int)(item.PetExperience * 0.9), minimumExp);
await this.ResetPetBehaviorAsync().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.

Comment thread src/GameLogic/Player.cs
Comment on lines +2013 to +2020
if (!isPvpDamage)
{
var randomDefensiveItem = this.Inventory?.EquippedItems.Where(ItemExtensions.IsDefensiveItem).SelectRandom();
if (randomDefensiveItem is { Durability: > 0.0 })
{
await this.DecreaseDefenseItemDurabilityAsync(randomDefensiveItem, damage).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.

Only mobs have m_AttackDamageMin > 0
zTeamS6.3, emu

I think this also makes sense, because otherwise all PvP would wear armor down.

Comment thread src/GameLogic/Player.cs
}

private async ValueTask DecreaseDefenseItemDurabilityAsync(Item targetItem, HitInfo hitInfo)
private async ValueTask DecreaseDefenseItemDurabilityAsync(Item targetItem, float damage)

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 thread src/GameLogic/Player.cs
await this.DestroyInventoryItemAsync(pet).ConfigureAwait(false);
}
}
await this.DecreasePetDurabilityAsync(pet, damage).ConfigureAwait(false);

@ze-dom ze-dom Sep 23, 2026 •

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

Unfortunately pet durability decrease is originally calculated in the middle of damage calcs, so we diverge here a bit.

Comment thread src/GameLogic/Player.cs
}

private async ValueTask DecreaseWeaponDurabilityAfterHitAsync()
private async ValueTask DecreasePetDurabilityAsync(Item pet, float damage)

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 thread src/GameLogic/Player.cs
Comment on lines +2129 to +2136
var item when item == ItemConstants.Angel
|| item == ItemConstants.Rudolf => 3,
var item when item == ItemConstants.Imp
|| item == ItemConstants.DarkHorse
|| item == ItemConstants.Fenrir => 2,
var item when item == ItemConstants.Dinorant => 10,
var item when item == ItemConstants.Demon => 1.5,
_ => 1,

@ze-dom ze-dom Sep 23, 2026 •

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

A little bit of divergence between the two sources regarding dinorant and the cash shop pets. Went with emu because it has the master skill increase in place, which is missing in zTeamS6.3.

Comment thread src/GameLogic/Player.cs
{
decrement = 1 + (damage * damageFactor / damagePerOnePetDurability);
decrement /= 1500 + itemDurationIncrease;
}

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 thread src/GameLogic/Player.cs
{
decrement = 1 + (damage * damageFactor / (damagePerOnePetDurability * itemDurationIncrease));
decrement /= attributes[Stats.ArcheryMaxDmg] > 0 ? 160 : 200;
}

@ze-dom ze-dom Sep 23, 2026 •

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

Elf-or-else divisor: emu

Comment thread src/GameLogic/Player.cs

await this._movement.StopWalkingAsync().ConfigureAwait(false);
await this.WarpToSafezoneAsync().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

Note: Icarus safezone map is actually Devias, not Lost Tower => todo

Comment thread src/GameLogic/Player.cs
}
}

private async ValueTask DecreaseWeaponDurabilityAfterHitAsync(float targetDefense, DamageType damageType)

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.

NormalWeaponDurabilityDown(): zTeamS6.3, emu
BowWeaponDurabilityDown(): zTeamS6.3, emu
StaffWeaponDurabilityDown(): zTeamS6.3, emu

Comment thread src/GameLogic/Player.cs
&& pu.Boost?.ConstantValue is { } socketInc)
{
defenseToDamageRatio -= defenseToDamageRatio * socketInc.Value;
}

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.GameConfiguration.HitsPerOneItemDurability = 10000;
this.GameConfiguration.DamagePerOneItemDurability = 69;
this.GameConfiguration.DamagePerOnePetDurability = 100;
this.GameConfiguration.HitsPerOneItemDurability = 564;

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.GameConfiguration.DamagePerOnePetDurability = 100000;
this.GameConfiguration.HitsPerOneItemDurability = 10000;
this.GameConfiguration.DamagePerOneItemDurability = 69;
this.GameConfiguration.DamagePerOnePetDurability = 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.

this.GameConfiguration.DamagePerOneItemDurability = 2000;
this.GameConfiguration.DamagePerOnePetDurability = 100000;
this.GameConfiguration.HitsPerOneItemDurability = 10000;
this.GameConfiguration.DamagePerOneItemDurability = 69;

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.

@ze-dom
ze-dom marked this pull request as ready for review September 23, 2026 16:23

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice work — the direction looks good to me: the time-decay plug-in, the durability → power-up factor, and the repair event fix all make sense, and the update plug-ins cover the three data versions. A quick review with a few things that stood out.

Blocking

  • ItemDurabilityRefactorPlugInSeason6.cs doesn't compile (assignment to a null-conditional access). That's very likely the failing MUnique.OpenMU check.
  • The *DurationIncrease stats are used as divisors while the master skills feed them values below 1, so putting points into the durability skills currently makes items decay faster (~8x at skill level 1). Details inline.

Worth a second look

  • Unit inconsistency across the four durability master skills (percent vs. factor, multiplied vs. added).
  • Possible NullReferenceException from ItemPowerUps.FirstOrDefault(...).Value in both durability paths.
  • GetRandomWeapon: the left-hand-staff case nulls the right hand.
  • Two behaviour changes that aren't in the description: Plasma Storm's 25% chance is now 100%, and defensive items no longer lose durability in PvP.
  • DurabilityThresholds cache: [NotMapped] vs. the repo's [Transient] convention, and manual invalidation that a few item.Level writers miss.

Nits

  • ItemDurabilityRefactorPlugIn075.ApplyAsync only calls base — the override can go; ItemDurabilityRefactorPlugInBase.ApplyAsync is async without an await (CS1998), plus a stray blank line before });.
  • Stale resources left behind: PlasmaStormSkillPlugIn_Name / _Description (plug-in deleted) and ItemBasePowerUpDefinition_BaseValueElement_* (property removed).
  • PlugInResources.Designer.cs lost its UTF-8 BOM, and the new .resx entry is indented by four spaces instead of two.
  • GetRandomWeapon mixes !(x ?? true) and !x ?? true in adjacent branches — both work, but they read as if one is a typo.
  • ItemPowerUpFactory:342 now uses IsArmorItem() where it was IsDefenseItemSlot && !IsJewelry, which drops wings from the excellent/ancient defense bonus. Harmless in practice, but it is a semantic change.

Generated by Claude Code

Comment on lines +68 to +70
levelOption.PowerUpDefinition?.TargetAttribute = weaponDurationIncrease;
levelOption.PowerUpDefinition?.Boost?.ConstantValue.Value -= 1.0f;
levelOption.PowerUpDefinition?.Boost?.ConstantValue.AggregateType = AggregateType.AddRaw;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't compile: a null-conditional access can't be the target of an assignment (CS0131), which applies to all three lines (including the compound -=). This is most likely why the MUnique.OpenMU build check is red.

Something like this works:

foreach (var levelOption in durationIncrease.LevelDependentOptions)
{
    if (levelOption.PowerUpDefinition is not { } powerUpDefinition)
    {
        continue;
    }

    powerUpDefinition.TargetAttribute = weaponDurationIncrease;
    if (powerUpDefinition.Boost?.ConstantValue is { } constantValue)
    {
        constantValue.Value -= 1.0f;
        constantValue.AggregateType = AggregateType.AddRaw;
    }
}

Generated by Claude Code

@ze-dom ze-dom Sep 25, 2026 •

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.

Is the pipeline using an older version of C#? This compiles on my machine.
Since C# 14 null-conditional assignment is allowed.

But I can change if needed.

Comment thread src/GameLogic/Player.cs
Comment on lines +2056 to 2060
float itemDurationIncrease = attributes[Stats.WeaponAndArmorDurationIncrease];
if (itemDurationIncrease == 0)
{
itemDurationIncrease = 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The duration-increase stat is used as a divisor, but the master skills now feed it values below 1, so investing skill points makes items decay faster.

DurabilityReduction1 (and DurabilityReduction1FistMaster) is AddRaw of Formula1204 / 100, which is ~0.12 at skill level 1 and ~0.90 at level 20. The == 0 → 1 guard only covers "no skill at all", so:

  • skill level 1 → decrement = ratio / (69 * 0.12) ⇒ ~8x faster durability loss than without the skill
  • skill level 20 → ratio / (69 * 0.90) ⇒ still ~11% faster than without the skill

I think it wants to be a bonus on top of 1:

float itemDurationIncrease = 1 + attributes[Stats.WeaponAndArmorDurationIncrease];

and then the if (itemDurationIncrease == 0) guard can go away.

The same pattern is in DecreaseWeaponDurabilityAfterHitAsync (line ~2201) and in DurabilityDecayPlugIn (JewelryAndWingsDurationIncrease), both fed by Formula1204 / 100 skills as well.


Generated by Claude Code

Comment thread src/GameLogic/Player.cs
if (identifier == ItemConstants.DarkHorse)
{
decrement = 1 + (damage * damageFactor / damagePerOnePetDurability);
decrement /= 1500 + itemDurationIncrease;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The four durability master skills end up with three different units, which makes their effects wildly inconsistent:

  • DurabilityReduction1/2 (+ fist master variants) → Formula1204 / 100 (0.12 … 0.90), used as a multiplicative divisor — see the separate comment, this one currently backfires.
  • DurabilityReduction3 / DurabilityReduction3FistMaster → raw Formula1204 (12 … 90) for PetDurationIncrease, also used as a multiplicative divisor (damagePerOnePetDurability * itemDurationIncrease) ⇒ 12x slower pet decay from a single skill point, up to 90x.
  • PetDurabilityStr → raw Formula1204 for TrainablePetDurationIncrease, but added to a constant here (1500 + …) and in DecreaseRavenDurabilityAfterHitAsync (800 + …) ⇒ a ~1–6% effect.

Was the missing / 100 on DurabilityReduction3/3FistMaster intentional? It would be good to settle on one unit (percentage or factor) for the whole family and apply it the same way in all consumers.


Generated by Claude Code

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's the way the sources go...

Comment thread src/GameLogic/Player.cs Outdated
Comment on lines +2071 to +2074
var itemPowerUps = attributes.ItemPowerUps.FirstOrDefault(ipu => ipu.Key == targetItem).Value;
itemDefense = attributes.GetComposableAttribute(Stats.DefenseBase)?.Elements
.Where(e => e.AggregateType == AggregateType.AddRaw && itemPowerUps.Contains(e))
.Sum(e => e.Value) ?? 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FirstOrDefault on a KeyValuePair returns default when the item isn't in the dictionary, so itemPowerUps can be null and itemPowerUps.Contains(e) then throws a NullReferenceException — the ?? 0 only guards GetComposableAttribute returning null.

TryGetValue avoids that and is O(1) instead of scanning the whole dictionary on every hit:

if (!attributes.ItemPowerUps.TryGetValue(targetItem, out var itemPowerUps))
{
    return;
}

Same at line ~2228 in DecreaseWeaponDurabilityAfterHitAsync.


Generated by Claude Code

Comment on lines +330 to +335
else
{
if (left?.Definition?.Group == StaffItemGroup || right?.Definition?.Group == StaffItemGroup)
{
right = null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This nulls the right hand when the left one holds the staff — is that intended, or should the staff itself (left) be excluded from taking durability loss on a non-magic hit? As written, a left-hand staff both survives the filter and removes the right-hand weapon from the selection, so the staff is always the one that loses durability.


Generated by Claude Code

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.

Intended. With the exception of Summoner, a LH staff is always the "main" weapon, magic or non-magic attack.

Comment thread src/DataModel/Entities/Item.cs Outdated
Comment on lines +41 to +42
[NotMapped]
public double[]? DurabilityThresholds { get; set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things about this cache:

  1. The repo's convention for non-persisted, run-time-only members is [Transient] (MUnique.OpenMU.Annotations), which both EfCoreModelGenerator and the generated MapsterConfigurator key off. [NotMapped] keeps it out of the EF schema, but the mapping layer will still copy the array reference between the entity and the data model. [NotMapped] is otherwise not used anywhere in DataModel.

  2. The cache is invalidated by hand in five places (UpgradeItemLevelJewelConsumeHandlerPlugIn, BaseItemCraftingHandler x2, SimpleItemCraftingHandler, ItemViewModel x2), and other places that change item.Level don't reset it — e.g. BaseEventTicketCrafting.cs:107 and the bot gear equippers. It'd be more robust to store the level (and ancient/excellent state) the thresholds were computed for and recompute on mismatch, so it can't go stale by omission.


Generated by Claude Code

Comment on lines +45 to +49
var previousFactor = item.GetCurrentDurabilityFactor();
item.Durability = item.GetMaximumDurabilityOfOnePiece();
if (previousFactor != item.GetCurrentDurabilityFactor())
{
await ((InventoryStorage?)player.Inventory)!.RaiseEquippedItemsChangedAsync(item, true).ConfigureAwait(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

RepairItemAsync(player, slot) can be called for any inventory slot, not just equipped ones. For a non-equipped item this now raises EquippedItemsChanged, which runs UpdateItemsOnChangeAsync → OnAppearanceChanged and broadcasts an appearance change to all observers for what is just a backpack repair. Worth guarding on the item actually being equipped.

Also, the ((InventoryStorage?)player.Inventory)! cast (here, below, and in Player.DecreaseItemDurabilityAsync) would be nicer as a member on IInventoryStorage — the interface is what everything else consumes.


Generated by Claude Code

Comment thread src/GameLogic/ItemPowerUpFactory.cs Outdated
Comment on lines +20 to +30
private readonly AttributeDefinition[] _durabilityAffectedItemAttributes =
[
Stats.DefenseBase,
Stats.DefenseShield,
Stats.DefenseRatePvm,
Stats.MinimumPhysBaseDmgByWeapon,
Stats.MaximumPhysBaseDmgByWeapon,
Stats.StaffRise,
Stats.ScepterRise,
Stats.BookRise,
];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: this can be static readonly, and since it's hit with Contains for every base power-up of every item, a HashSet<AttributeDefinition> (or FrozenSet) would avoid the linear scan.


Generated by Claude Code

ItemDurabilityRefactor075 = 115,

/// <summary>
/// The version of the <see cref="ItemDurabilityRefactorPlugIn095D"/>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: the class is ItemDurabilityRefactorPlugIn095d (lower-case d), so this cref doesn't resolve (CS1574).


Generated by Claude Code

Comment on lines +40 to +56
if (attributes[Stats.IsInSafezone] < 1 || isTransformationRing || item.IsWing())
{
double decrementWeight = identifier switch
{
var _ when isTransformationRing => 11.28, // 11.28 / 564 = 0.02
var itm when itm == ItemConstants.WizardsRing => 7,
var itm when itm == ItemConstants.MoonstonePendant => 6.3,
_ => 0.1,
};

var durationIncrease = attributes[Stats.JewelryAndWingsDurationIncrease];
if (durationIncrease == 0 || isTransformationRing)
{
durationIncrease = 1;
}

var decrement = decrementWeight / (player.GameContext.Configuration.HitsPerOneItemDurability * durationIncrease);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two notes here:

  • durationIncrease has the same divisor problem as the weapon/armor path: JewelryAndWingsDurationIncrease comes from DurabilityReduction2 as AddRaw of Formula1204 / 100 (0.12 … 0.90), so one skill point makes jewelry and wings decay up to ~8x faster. Looks like it should be 1 + attributes[...].
  • The condition means wings decay even inside a safezone, while jewelry doesn't. Is that deliberate (i.e. wings wear out while idling in town too)? A short comment with the source reference would help, like the ones above.

Generated by Claude Code

@ze-dom

ze-dom commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Think I've addressed everything. All set.

This branch has not been deployed

No deployments
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