Conversation
| bool isPvpDamage = attacker is Player or IPlayerSurrogate; | ||
| float damage = isPvpDamage ? hitInfo.TotalDamage : attacker.Attributes[Stats.MinimumPhysBaseDmg]; | ||
| await this.DecreaseItemDurabilityAfterHitAsync(damage, isPvpDamage, skill).ConfigureAwait(false); |
There was a problem hiding this comment.
zTeamS6.3: ObjAttack.cpp, user.cpp
emu: ObjAttack.cpp, user.cpp
| if (attacker is Player player) | ||
| { | ||
| await player.AfterHitTargetAsync().ConfigureAwait(false); | ||
| await player.AfterHitTargetAsync(this.Attributes[Stats.DefensePvm], skill?.Skill?.DamageType).ConfigureAwait(false); |
There was a problem hiding this comment.
zTeamS6.3: ObjAttack.cpp, user.cpp
emu: ObjAttack.cpp, user.cpp
| if (attacker as IPlayerSurrogate is { } playerSurrogate) | ||
| { | ||
| await playerSurrogate.Owner.AfterHitTargetAsync().ConfigureAwait(false); | ||
| await playerSurrogate.Owner.DecreaseRavenDurabilityAfterHitAsync(hitInfo.TotalDamage).ConfigureAwait(false); |
| this.AddRandomExcellentOptions(resultItem, player); | ||
|
|
||
| resultItem.Durability = craftingResultItem.Durability ?? resultItem.GetMaximumDurabilityOfOnePiece(); | ||
|
|
There was a problem hiding this comment.
Durability depends on item being excellent
| { | ||
| double decrementWeight = new ItemIdentifier(item.Definition!.Number, item.Definition.Group) switch | ||
| { | ||
| var _ when isTransformationRing => 11.28, // 11.28 / 564 = 0.02 |
| durationIncrease = 1; | ||
| } | ||
|
|
||
| var decrement = decrementWeight / (player.GameContext.Configuration.HitsPerOneItemDurability * durationIncrease); |
| /// </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); |
| /// 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); |
| /// 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); |
| /// <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); |
There was a problem hiding this comment.
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, |
| { | ||
| 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, |
| 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 }; | ||
|
|
| if (!(left?.IsWizardryWeapon(out _) ?? true) || (left?.CanHaveSkill() ?? false)) | ||
| { | ||
| left = null; | ||
| } |
There was a problem hiding this comment.
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]; |
| { | ||
| if (item.Durability < item.DurabilityThresholds[i]) | ||
| { | ||
| return 1 - DurabilityFactors[^(i + 1)]; |
| Stats.StaffRise, | ||
| Stats.ScepterRise, | ||
| Stats.BookRise, | ||
| ]; |
There was a problem hiding this comment.
zTeamS6.3: zzzitem.cpp, ObjAttack.cpp, DarkSpirit.cpp
emu: zzzitem.cpp, ObjAttack.cpp, DarkSpirit.cpp
|
|
||
| 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; |
There was a problem hiding this comment.
zTeamS6.3: zzzitem.cpp (regular item options - except rings and wings hp rec), zzzitem.cpp (ancient bonus opt)
emu: zzzitem.cpp, zzzitem.cpp
| if (attacker is Player attackerPlayer) | ||
| { | ||
| await attackerPlayer.AfterHitTargetAsync().ConfigureAwait(false); | ||
| await attackerPlayer.AfterHitTargetAsync(this.Attributes[Stats.DefenseFinal], skill?.Skill?.DamageType).ConfigureAwait(false); |
There was a problem hiding this comment.
zTeamS6.3: ObjAttack.cpp, user.cpp
emu: ObjAttack.cpp, user.cpp
| item.PetExperience = (int)Math.Max((int)(item.PetExperience * 0.9), minimumExp); | ||
| await this.ResetPetBehaviorAsync().ConfigureAwait(false); | ||
| } | ||
| } |
| if (!isPvpDamage) | ||
| { | ||
| var randomDefensiveItem = this.Inventory?.EquippedItems.Where(ItemExtensions.IsDefensiveItem).SelectRandom(); | ||
| if (randomDefensiveItem is { Durability: > 0.0 }) | ||
| { | ||
| await this.DecreaseDefenseItemDurabilityAsync(randomDefensiveItem, damage).ConfigureAwait(false); | ||
| } | ||
| } |
| } | ||
|
|
||
| private async ValueTask DecreaseDefenseItemDurabilityAsync(Item targetItem, HitInfo hitInfo) | ||
| private async ValueTask DecreaseDefenseItemDurabilityAsync(Item targetItem, float damage) |
| await this.DestroyInventoryItemAsync(pet).ConfigureAwait(false); | ||
| } | ||
| } | ||
| await this.DecreasePetDurabilityAsync(pet, damage).ConfigureAwait(false); |
| } | ||
|
|
||
| private async ValueTask DecreaseWeaponDurabilityAfterHitAsync() | ||
| private async ValueTask DecreasePetDurabilityAsync(Item pet, float damage) |
| 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, |
| { | ||
| decrement = 1 + (damage * damageFactor / damagePerOnePetDurability); | ||
| decrement /= 1500 + itemDurationIncrease; | ||
| } |
| { | ||
| decrement = 1 + (damage * damageFactor / (damagePerOnePetDurability * itemDurationIncrease)); | ||
| decrement /= attributes[Stats.ArcheryMaxDmg] > 0 ? 160 : 200; | ||
| } |
|
|
||
| await this._movement.StopWalkingAsync().ConfigureAwait(false); | ||
| await this.WarpToSafezoneAsync().ConfigureAwait(false); | ||
| } |
| } | ||
| } | ||
|
|
||
| private async ValueTask DecreaseWeaponDurabilityAfterHitAsync(float targetDefense, DamageType damageType) |
| && pu.Boost?.ConstantValue is { } socketInc) | ||
| { | ||
| defenseToDamageRatio -= defenseToDamageRatio * socketInc.Value; | ||
| } |
There was a problem hiding this comment.
zTeamS6.3: zzzitem.cpp, ItemSocketOptionSystem.cpp
emu: zzzitem.cpp,, SocketOptionSystem.cpp
| this.GameConfiguration.HitsPerOneItemDurability = 10000; | ||
| this.GameConfiguration.DamagePerOneItemDurability = 69; | ||
| this.GameConfiguration.DamagePerOnePetDurability = 100; | ||
| this.GameConfiguration.HitsPerOneItemDurability = 564; |
There was a problem hiding this comment.
zTeamS6.3: NormalWeaponDurabilityDown() (weapons), DurabilityDown() (jewelry and wings)
emu: NormalWeaponDurabilityDown(), DurabilityDown()
| this.GameConfiguration.DamagePerOnePetDurability = 100000; | ||
| this.GameConfiguration.HitsPerOneItemDurability = 10000; | ||
| this.GameConfiguration.DamagePerOneItemDurability = 69; | ||
| this.GameConfiguration.DamagePerOnePetDurability = 100; |
| this.GameConfiguration.DamagePerOneItemDurability = 2000; | ||
| this.GameConfiguration.DamagePerOnePetDurability = 100000; | ||
| this.GameConfiguration.HitsPerOneItemDurability = 10000; | ||
| this.GameConfiguration.DamagePerOneItemDurability = 69; |
sven-n
left a comment
There was a problem hiding this comment.
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.csdoesn't compile (assignment to a null-conditional access). That's very likely the failingMUnique.OpenMUcheck.- The
*DurationIncreasestats 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
NullReferenceExceptionfromItemPowerUps.FirstOrDefault(...).Valuein 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.
DurabilityThresholdscache:[NotMapped]vs. the repo's[Transient]convention, and manual invalidation that a fewitem.Levelwriters miss.
Nits
ItemDurabilityRefactorPlugIn075.ApplyAsynconly callsbase— the override can go;ItemDurabilityRefactorPlugInBase.ApplyAsyncisasyncwithout anawait(CS1998), plus a stray blank line before});.- Stale resources left behind:
PlasmaStormSkillPlugIn_Name/_Description(plug-in deleted) andItemBasePowerUpDefinition_BaseValueElement_*(property removed). PlugInResources.Designer.cslost its UTF-8 BOM, and the new.resxentry is indented by four spaces instead of two.GetRandomWeaponmixes!(x ?? true)and!x ?? truein adjacent branches — both work, but they read as if one is a typo.ItemPowerUpFactory:342now usesIsArmorItem()where it wasIsDefenseItemSlot && !IsJewelry, which drops wings from the excellent/ancient defense bonus. Harmless in practice, but it is a semantic change.
Generated by Claude Code
| levelOption.PowerUpDefinition?.TargetAttribute = weaponDurationIncrease; | ||
| levelOption.PowerUpDefinition?.Boost?.ConstantValue.Value -= 1.0f; | ||
| levelOption.PowerUpDefinition?.Boost?.ConstantValue.AggregateType = AggregateType.AddRaw; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| float itemDurationIncrease = attributes[Stats.WeaponAndArmorDurationIncrease]; | ||
| if (itemDurationIncrease == 0) | ||
| { | ||
| itemDurationIncrease = 1; | ||
| } |
There was a problem hiding this comment.
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
| if (identifier == ItemConstants.DarkHorse) | ||
| { | ||
| decrement = 1 + (damage * damageFactor / damagePerOnePetDurability); | ||
| decrement /= 1500 + itemDurationIncrease; |
There was a problem hiding this comment.
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→ rawFormula1204(12 … 90) forPetDurationIncrease, also used as a multiplicative divisor (damagePerOnePetDurability * itemDurationIncrease) ⇒ 12x slower pet decay from a single skill point, up to 90x.PetDurabilityStr→ rawFormula1204forTrainablePetDurationIncrease, but added to a constant here (1500 + …) and inDecreaseRavenDurabilityAfterHitAsync(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
There was a problem hiding this comment.
That's the way the sources go...
| 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; |
There was a problem hiding this comment.
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
| else | ||
| { | ||
| if (left?.Definition?.Group == StaffItemGroup || right?.Definition?.Group == StaffItemGroup) | ||
| { | ||
| right = null; | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Intended. With the exception of Summoner, a LH staff is always the "main" weapon, magic or non-magic attack.
| [NotMapped] | ||
| public double[]? DurabilityThresholds { get; set; } |
There was a problem hiding this comment.
Two things about this cache:
-
The repo's convention for non-persisted, run-time-only members is
[Transient](MUnique.OpenMU.Annotations), which bothEfCoreModelGeneratorand the generatedMapsterConfiguratorkey 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 inDataModel. -
The cache is invalidated by hand in five places (
UpgradeItemLevelJewelConsumeHandlerPlugIn,BaseItemCraftingHandlerx2,SimpleItemCraftingHandler,ItemViewModelx2), and other places that changeitem.Leveldon't reset it — e.g.BaseEventTicketCrafting.cs:107and 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
| var previousFactor = item.GetCurrentDurabilityFactor(); | ||
| item.Durability = item.GetMaximumDurabilityOfOnePiece(); | ||
| if (previousFactor != item.GetCurrentDurabilityFactor()) | ||
| { | ||
| await ((InventoryStorage?)player.Inventory)!.RaiseEquippedItemsChangedAsync(item, true).ConfigureAwait(false); |
There was a problem hiding this comment.
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
| private readonly AttributeDefinition[] _durabilityAffectedItemAttributes = | ||
| [ | ||
| Stats.DefenseBase, | ||
| Stats.DefenseShield, | ||
| Stats.DefenseRatePvm, | ||
| Stats.MinimumPhysBaseDmgByWeapon, | ||
| Stats.MaximumPhysBaseDmgByWeapon, | ||
| Stats.StaffRise, | ||
| Stats.ScepterRise, | ||
| Stats.BookRise, | ||
| ]; |
There was a problem hiding this comment.
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"/>. |
There was a problem hiding this comment.
Nit: the class is ItemDurabilityRefactorPlugIn095d (lower-case d), so this cref doesn't resolve (CS1574).
Generated by Claude Code
| 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); |
There was a problem hiding this comment.
Two notes here:
durationIncreasehas the same divisor problem as the weapon/armor path:JewelryAndWingsDurationIncreasecomes fromDurabilityReduction2asAddRawofFormula1204 / 100(0.12 … 0.90), so one skill point makes jewelry and wings decay up to ~8x faster. Looks like it should be1 + 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
|
Think I've addressed everything. All set. |




To-do
Developments
Bugfixes
EquippedItemsChanged. Previously repaired items w/ durability = 0 were not removing their power-ups.