Skip to content

refactor(model): extract damage calculation into the pure domain model#16

Merged
nbdSteve merged 3 commits into
masterfrom
fm/armorplus-4d-damagecalc-e7
Jul 22, 2026
Merged

refactor(model): extract damage calculation into the pure domain model#16
nbdSteve merged 3 commits into
masterfrom
fm/armorplus-4d-damagecalc-e7

Conversation

@nbdSteve

Copy link
Copy Markdown
Owner

Intent

Extract the pure damage-calculation logic from BasicSetData and HandSetData into the domain model layer (model.ability.BasicDamageCalculator and ArmorHandItem.calculateFinalDamage), using the strangler-fig pattern. The existing classes become thin delegators so the plugin behaves identically. Behavior preservation is the top priority - the characterization tests (BasicSetDataTest, HandSetDataTest) must pass unchanged. The new pure domain logic gets 100% coverage enforced by the JaCoCo core gate. BasicSetData.onHit delegates the basic-increase non-hand-item path to BasicDamageCalculator.calculateAttackDamage; the hand-item path still calls through HandSetData.calculateFinalDamage (which itself now delegates to ArmorHandItem). BasicSetData.onDamage delegates to BasicDamageCalculator.calculateDefense for the reduction math, then applies the returned knockback multiplier to the Bukkit vector. The design intentionally does NOT wire through port interfaces yet - this is pure math extraction only.

What Changed

  • Extracted the damage-calculation math into the Bukkit-free domain layer: new model.ability.BasicDamageCalculator handles the attack-damage multiplier and defense reduction plus knockback multiplier, and ArmorHandItem.calculateFinalDamage() handles the hand-item combined multiplier.
  • BasicSetData.onHit/onDamage and HandSetData.calculateFinalDamage are now thin delegators that translate Bukkit events into domain calls and apply the results back, with the -1 "disabled" sentinel checked once per path (a review pass deduplicated the redundant checks). Characterization tests (BasicSetDataTest, HandSetDataTest) pass unchanged, and a differential harness comparing the old formulas against the new domain code across 1088 input combinations found no mismatches.
  • Added BasicDamageCalculatorTest and included BasicDamageCalculator in the JaCoCo check-core gate enforcing 100% line and branch coverage; AGENTS.md/README.md docs updated to describe the new domain home for damage logic.

Risk Assessment

✅ Low: Straightforward strangler-fig extraction of pure math into a new domain class with complete test coverage, verified behavioral equivalence to the original code, and no new logic introduced in the delegators.

Testing

Ran the full mvn clean verify build (212 tests plus the JaCoCo check-core 100% gate) on JDK 25, re-ran the unchanged BasicSetData/HandSetData characterization tests plus the new BasicDamageCalculatorTest in isolation, and executed a custom differential sweep comparing the base commit's original damage formulas against the new domain classes across 1088 input combinations - everything passes with zero numerical mismatches, and the shaded plugin jar ships the new Bukkit-free domain classes as Java 8 bytecode. No screenshot evidence because this is a headless Minecraft server plugin change with no rendered UI; the differential-sweep transcript is the end-user-behavior evidence.

Evidence: Differential sweep: base-commit formulas vs new domain classes (1088 combos, 0 mismatches)

Sample rows (old formula -> new domain result): attack: damage=7.00 increase=1.30 old=9.1000 new=9.1000 attack: damage=7.00 increase=-1.00 old=7.0000 new=7.0000 defense: damage=10.00 reduction=0.50 kb=2.00 old=5.0000 new=5.0000 (kb passthrough=2.00) defense sentinel: damage=10.00 reduction=-1 kb=-1 old=10.0000 new=10.0000 hand: damage=8.00 handIncrease=1.50 setIncrease=1.30 old=14.4000 new=14.4000 hand sentinel: damage=8.00 handIncrease=1.50 setIncrease=-1 old=12.0000 new=12.0000 Checked 1088 input combinations, 0 mismatches. RESULT: new domain logic is numerically identical to the base-commit formulas.

== Differential sweep: base-commit formulas vs new domain classes ==

Sample rows (old formula -> new domain result):
  attack: damage=7.00 increase=1.30  old=9.1000 new=9.1000
  attack: damage=7.00 increase=-1.00  old=7.0000 new=7.0000
  attack: damage=19.50 increase=0.50  old=9.7500 new=9.7500
  defense: damage=10.00 reduction=0.50 kb=2.00  old=5.0000 new=5.0000 (kb passthrough=2.00)
  defense sentinel: damage=10.00 reduction=-1 kb=-1  old=10.0000 new=10.0000
  hand: damage=8.00 handIncrease=1.50 setIncrease=1.30  old=14.4000 new=14.4000
  hand sentinel: damage=8.00 handIncrease=1.50 setIncrease=-1  old=12.0000 new=12.0000

Checked 1088 input combinations, 0 mismatches.
RESULT: new domain logic is numerically identical to the base-commit formulas.
Evidence: Characterization + domain test run (unchanged tests, all green)

Running gg.steve.mc.ap.model.ability.BasicDamageCalculatorTest Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 Running gg.steve.mc.ap.data.types.HandSetDataTest Tests run: 15, Failures: 0, Errors: 0, Skipped: 0 Running gg.steve.mc.ap.data.BasicSetDataTest Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 Tests run: 36, Failures: 0, Errors: 0, Skipped: 0

[INFO] Running gg.steve.mc.ap.model.ability.BasicDamageCalculatorTest
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.030 s -- in gg.steve.mc.ap.model.ability.BasicDamageCalculatorTest
[INFO] Running gg.steve.mc.ap.data.types.HandSetDataTest
[INFO] Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.810 s -- in gg.steve.mc.ap.data.types.HandSetDataTest
[INFO] Running gg.steve.mc.ap.data.BasicSetDataTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.088 s -- in gg.steve.mc.ap.data.BasicSetDataTest
[INFO] Tests run: 36, Failures: 0, Errors: 0, Skipped: 0
Evidence: JaCoCo core-gate coverage for extracted classes (0 missed lines/branches)
JaCoCo coverage for extracted domain classes (missed columns are INSTRUCTION_MISSED,BRANCH_MISSED,LINE_MISSED,...):
GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED
ArmorPlus,gg.steve.mc.ap.model.ability,ArmorHandItem,0,20,0,2,0,4,0,2,0,1
ArmorPlus,gg.steve.mc.ap.model.ability,BasicDamageCalculator,0,26,0,4,0,5,0,4,0,2

check-core gate result from mvn verify:
[INFO] Analyzed bundle 'ArmorPlus' with 79 classes
[INFO] All coverage checks have been met.
[INFO] ------------------------------------------------------------------------
Evidence: JaCoCo HTML coverage report for BasicDamageCalculator
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>BasicDamageCalculator</title><script type="text/javascript" src="../jacoco-resources/sort.js"></script></head><body onload="initialSort(['breadcrumb'])"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ArmorPlus</a> &gt; <a href="index.html" class="el_package">gg.steve.mc.ap.model.ability</a> &gt; <span class="el_class">BasicDamageCalculator</span></div><h1>BasicDamageCalculator</h1><table class="coverage" cellspacing="0" id="coveragetable"><thead><tr><td class="sortable" id="a" onclick="toggleSort(this)">Element</td><td class="down sortable bar" id="b" onclick="toggleSort(this)">Missed Instructions</td><td class="sortable ctr2" id="c" onclick="toggleSort(this)">Cov.</td><td class="sortable bar" id="d" onclick="toggleSort(this)">Missed Branches</td><td class="sortable ctr2" id="e" onclick="toggleSort(this)">Cov.</td><td class="sortable ctr1" id="f" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="g" onclick="toggleSort(this)">Cxty</td><td class="sortable ctr1" id="h" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="i" onclick="toggleSort(this)">Lines</td><td class="sortable ctr1" id="j" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="k" onclick="toggleSort(this)">Methods</td></tr></thead><tfoot><tr><td>Total</td><td class="bar">0 of 26</td><td class="ctr2">100%</td><td class="bar">0 of 4</td><td class="ctr2">100%</td><td class="ctr1">0</td><td class="ctr2">4</td><td class="ctr1">0</td><td class="ctr2">5</td><td class="ctr1">0</td><td class="ctr2">2</td></tr></tfoot><tbody><tr><td id="a1"><a href="BasicDamageCalculator.java.html#L18" class="el_method">calculateDefense(double, double, double)</a></td><td class="bar" id="b0"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="16" alt="16"/></td><td class="ctr2" id="c0">100%</td><td class="bar" id="d0"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="2" alt="2"/></td><td class="ctr2" id="e0">100%</td><td class="ctr1" id="f0">0</td><td class="ctr2" id="g0">2</td><td class="ctr1" id="h0">0</td><td class="ctr2" id="i1">2</td><td class="ctr1" id="j0">0</td><td class="ctr2" id="k0">1</td></tr><tr><td id="a0"><a href="BasicDamageCalculator.java.html#L10" class="el_method">calculateAttackDamage(double, double)</a></td><td class="bar" id="b1"><img src="../jacoco-resources/greenbar.gif" width="75" height="10" title="10" alt="10"/></td><td class="ctr2" id="c1">100%</td><td class="bar" id="d1"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="2" alt="2"/></td><td class="ctr2" id="e1">100%</td><td class="ctr1" id="f1">0</td><td class="ctr2" id="g1">2</td><td class="ctr1" id="h1">0</td><td class="ctr2" id="i0">3</td><td class="ctr1" id="j1">0</td><td class="ctr2" id="k1">1</td></tr></tbody></table><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.15.202606040825</span></div></body></html>

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed ✅
  • ⚠️ src/main/java/gg/steve/mc/ap/data/BasicSetData.java:37 - The '-1 means disabled' sentinel decision is duplicated across both layers: onHit calls BasicDamageCalculator.calculateAttackDamage unconditionally (which internally checks increase != -1) and then re-checks this.increase != -1 before applying the result. The same computation is done and discarded when disabled. Guarding once - 'if (this.increase != -1) event.setDamage(BasicDamageCalculator.calculateAttackDamage(event.getDamage(), this.increase))' - preserves behavior (the characterization test only requires that setDamage is never called when disabled) and keeps the disabled-check in a single place. Same pattern applies to onDamage/calculateDefense.
  • ℹ️ src/main/java/gg/steve/mc/ap/model/ability/BasicDamageCalculator.java:19 - The ternary 'double kb = (knockback != -1) ? knockback : -1;' is an identity no-op - both branches yield knockback, so it is equivalent to 'double kb = knockback;'. It exists only so the -1 sentinel flows through CombatDamageModification for the delegator to re-check, which also conflicts with CombatDamageModification's javadoc convention that absence-of-modification should be modeled with Optional rather than magic values. Consider dropping the ternary now and modeling 'knockback disabled' explicitly (e.g. Optional or a boolean) in a follow-up.
  • ℹ️ src/main/java/gg/steve/mc/ap/data/types/HandSetData.java:41 - calculateFinalDamage builds a fresh ArmorHandItem (including a DamageCauseId with commons-lang3 validation) on every damage event, and the requireSet/damageCause fields it populates are unused by the calculation. Per-hit allocation is negligible for the JVM and caching would be complicated by HandSetData's mutable setters (setIncrease/setActiveCause), so per-call construction is a reasonable tradeoff - noting it as an accepted cost of the strangler-fig step.

🔧 Fix: deduplicate -1 sentinel checks in damage delegators
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • mvn -B clean verify with JDK 25 - full build, all 212 tests, and the JaCoCo check-core 100% line+branch gate (now including BasicDamageCalculator) pass
  • mvn -B test -Dtest=&#39;BasicSetDataTest,HandSetDataTest,BasicDamageCalculatorTest&#39; - 36 tests pass, including the 27 characterization tests pinning onHit/onDamage/calculateFinalDamage behavior
  • git diff 9553adf..be0e8f9 -- BasicSetDataTest.java HandSetDataTest.java - empty, proving characterization tests were not modified to accommodate the refactor
  • Differential harness (DiffSweep.java) executing the base commit's verbatim formulas vs BasicDamageCalculator.calculateAttackDamage/calculateDefense and ArmorHandItem.calculateFinalDamage across 1088 input combinations including -1 sentinels - 0 mismatches
  • Verified delegation wiring in the diff: onHit non-hand path -> calculateAttackDamage, hand path -> HandSetData -> ArmorHandItem, onDamage -> calculateDefense + Bukkit knockback vector
  • grep -rE &#39;import (org.bukkit|net.minecraft|de.tr7zw)&#39; src/main/java/gg/steve/mc/ap/model/ - zero forbidden imports in the domain model
  • unzip -l target/ArmorPlus-v2.3.6.jar + javap -v - new domain classes present in the shaded plugin jar as Java 8 bytecode (major version 52)
  • Inspected target/site/jacoco/jacoco.csv - 0 missed instructions/branches/lines for BasicDamageCalculator and ArmorHandItem
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Stephen Goodhill added 3 commits July 10, 2026 11:23
…Data/HandSetData now delegate

BasicDamageCalculator in model.ability holds the pure damage math:
- calculateAttackDamage: basic damage multiplier (increase != -1 guard)
- calculateDefense: reduction multiplier + knockback sentinel

HandSetData.calculateFinalDamage() now delegates to ArmorHandItem in the
domain layer. BasicSetData.onHit/onDamage delegate to BasicDamageCalculator
for the non-hand-item paths and defense calculations.

The existing characterization tests (BasicSetDataTest, HandSetDataTest)
pass unchanged, proving behavior preservation. New BasicDamageCalculatorTest
provides 100% line+branch coverage, enforced by the JaCoCo core gate.
@sonarqubecloud

Copy link
Copy Markdown

@nbdSteve
nbdSteve merged commit 80aca19 into master Jul 22, 2026
2 checks passed
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