v0.0.3: cut game-version bump cost to one command + a delta checklist#59
Merged
Conversation
… work Break down the v0.0.1->v0.0.2 +101k diff: ~92k is generated data, ~5k unrelated feature/cleanup work, and only ~2-3k human-written bump work. Split that residue into intrinsic cost (engine formula drift, reducible only in discovery) and self-inflicted cost (count-pins, per-version overlay copying, half-scripted drill, generated diff noise), then rank the v0.0.3 fixes: snapshot-blessed test pins, vendor calc-delta report, overlay-common layer, one-command bump script, gitattributes.
Generated JSON (data/**/generated, extractor overlay output, recaptured fixture meta.json) stays committed for runtime/deploy, but no longer renders as reviewable diff noise. Hand-curated overlay special_mods.json is explicitly exempted.
Data-content count pins (section counts, coverage floats, data-derived values) break mechanically on every data regen and their repair carries zero information. Introduce data/<ver>/generated/test_pins.json (flat map: pin name -> JSON value) plus a small helper: - test_pins::assert_pin(version_dir, name, actual) compares against the blessed snapshot and fails with a bless hint on mismatch; - POBR_BLESS_PINS=1 writes the actual value back (insta-style, no new dependency; write is serialized in-process, bless via cargo test). Structural guards (schemas, monotonic ratchets, invariants) stay as code assertions by design.
Move hand-pinned literals that mirror current data content into data/4.5.4.3/generated/test_pins.json: - parser_rules real_data_section_counts: all section lengths, distinct form count and unsupported lists -> parser_rules.section_counts pin; the structural guard (core form ids present) stays in code. - parser_rules handler_fallback_within_budget: the exact count (3) -> parser_rules.handler_fallbacks pin; the <=15 budget guard stays. - minions zombie/spectre coefficients (life/damage/armour, e.g. the 0.5.4b spectre life 3.0->2.2 rebalance) -> minions.* pins. Initial snapshot blessed from the current data; values match the previous literals exactly.
Remaining assertions that broke (or would break) on data bumps, fixed
without snapshots because their load-bearing meaning is version-stable:
- pobr-core any-attribute: assert the invariant (no net contribution:
zero mods or leftover, never a concrete attribute mod) instead of the
version-specific shape (empty mods vs leftover).
- precompile coverage baseline: the test asserted baseline ==
committed ratio, turning a deliberate ratchet into a pin that broke
whenever coverage rose. Assert the ratchet direction instead
(committed >= baseline, same semantics as regen-check.sh); raising
the baseline stays a human decision, like parity_no_regression.
- precompile --check repo test: derive the checked versions from
pobr_data::{DATA_VERSION, GOLDEN_PARITY_DATA_VERSION} instead of a
hand-pinned list (also stops sweeping stale data/ dirs).
- wasm tier degradation test: keep the deliberate 4.5.0.3.4 legacy-pack
fixture pin but skip gracefully if that pack is ever removed.
Run the migrated pin tests with POBR_BLESS_PINS=1 at the end of regen-all.sh so a data regen refreshes generated/test_pins.json in the same pass (soft_step: failures are recorded, not fatal). Uses cargo test (single process; the helper serializes writes in-process) rather than nextest, which runs one process per test and would race on the snapshot file.
bump-version.sh chains the whole drill that previously lived in memory notes: patch query, .dat download, tree/vendor alignment, regen-all (which now blesses test pins), CURRENT + DATA_VERSION advance, zh-CN and web data sync, then a targeted verification pass. Golden flip, vendor pin choice, and engine adaptation stay explicit manual decisions printed in the final summary.
… layer Load special_mods.json in two layers: version-independent curated data/overlay-common/special_mods.json merged under version-specific data/<ver>/overlay/special_mods.json. Version entries override common by id, others append; both absent yields None (unchanged soft-degrade). Adds overlay_common_path() helper (sibling of the version dir; resolves to overlay-common/<rel> for the in-memory wasm backend). Covers inherit / override-by-id / common-only / both-absent behaviors in load_special_mods. Part of P1-3 (docs/version-bump-architecture.md): curated entries carry forward to new data versions without manual per-version copying.
special_mods is read in several places outside the gamedata loader; each must see the version-independent overlay-common layer so the migration is behavior-preserving: - precompile-mods check.rs: prepend overlay-common in the compile set; precompile.rs test harness mirrors overlay-common into the isolated tmp (its GameData-based coverage recompute resolves the sibling layer there). - sync-pob-catalog extract dedup + special_mods_patterns test: read overlay-common so re-extraction dedups against curated entries and pattern validation covers them. - pobr-core parser support + special_mods_gate + repo compile smoke test: merge overlay-common under the version layer (same id-override semantics as the loader) since pobr-core cannot depend on pobr-gamedata. - web sync-data.mjs: copy data/overlay-common into the web bundle and list it in the manifest so the wasm in-memory backend inherits it. - regen-all.sh: drop special_mods from the per-version carry-forward list; overlay-common replaces that manual step. - docs/contributing-mods.md: document overlay-common as where curated entries go. Part of P1-3 (docs/version-bump-architecture.md).
133 special_mods entries present and identical in both 4.5.0.3.4 and 4.5.4.3 are version-independent curated fixes; move them to data/overlay-common/special_mods.json. The 16 entries unique to 4.5.4.3 stay in its version overlay; 4.5.0.3.4's overlay file (all-shared, now empty) is removed and its 133 entries are inherited from overlay-common via the loader merge. Merged load per version is entry-equivalent to the pre-migration originals (4.5.4.3: 149, 4.5.0.3.4: 133; ids + content identical, no duplicates). Part of P1-3 (docs/version-bump-architecture.md).
Add diff-vendor-calcs.sh: compares two PoB2 vendor pins and emits a markdown triage list, turning post-patch engine adaptation into a checklist instead of parity-drop archaeology. Report sections: - Calc module diff: Modules/Calc*.lua + ModParser.lua diffstat + folded hunks (collapse to stat-only past MAX_HUNK_LINES). - Data module diff: Data/ + Modules/Data*.lua ranked by churn, no hunks. - Extracted overlay diff: run extract-lua (special-mods/parser-rules/ uniques) against both pins; entity-level add/remove/change per collection via json_entry_diff.py. Details: - Each pin is a shallow git checkout in .cache/vendor-delta/<sha>/; codeload tarballs strip the working tree the headless extractor's HeadlessWrapper bootstrap needs (yields "modLib.parseMod missing"). Retries transient fetch EOF; writes a per-pin .pob2-version.txt for extract-lua --version-file. - Soft-degrade (regen-all.sh convention): an extract that fails on the old pin is noted, the report still generates. - Ignore /.cache/; document usage in pipeline/README.md. - Include sample report a82a33b -> ce8bffab (PoB 0.21.0 -> 0.22.0).
Move buff_definitions, high_precision_mods, local_mods and vendor_name_aliases into version-independent data/overlay-common/ (all four were byte-identical across the two shipped data versions, so both version copies are removed). Loaders inherit the common layer: - buff_definitions: two-layer id-merge like special_mods (common base + version-specific override/append). - high_precision_mods / local_mods: single-object domains, version-first with overlay-common fallback via new GameData::load_overlay_or_common. - vendor_name_aliases has no runtime loader (doc source-of-truth asset only). Extract paths::merge_by_key generic helper, shared by special_mods and buff_definitions, replacing the special_mods-local merge copy. Content-preserving (pure file moves); parity_no_regression unchanged.
- regen-all.sh step 6: drop the per-version carry-forward loop; all four curated overlay domains now live in data/overlay-common/ (inherited at load). - version-bump-drill.sh 3b: resolve check-buff-refs --defs to the version overlay if present, else the overlay-common copy. - extract_parser_rules.rs: update vendor_name_aliases source-of-truth pointer. - contributing-mods.md: document the four additional common-layer domains.
Phase 0 ignored deflection_matches_golden with a ~0.60x DeflectionRating gap on monk-martial-artist-twister. Re-triage (fresh pob2-oracle run, 2026-07-17) shows the gap was entirely downstream of the Evasion gap closed by the Mageblood work (6685c30): rating derives from Evasion x EvasionGainAsDeflection% and the formula itself (CalcDefence.lua:48-54 / :1516-1522) is unchanged in 0.5.4b. Current numbers vs 4.5.4.3 golden: monk rating 22267.8 vs 22312.08 (0.998x), huntress 5664.9 vs 5666.7, warrior 0.84 exact, sorceress 0. No engine change needed; stale 0.5.0-era comment values and drifted vendor line refs (0.21.0 -> 0.22.0) refreshed alongside.
The one genuine deflection-side gap left after re-triage: wolf-pack DeflectChance 48 vs 60 (0.80x) with Evasion already exact. Oracle defenceModList pins the missing source to Skill:SupportRefractionPlayerTwo - the Refraction I/II support's Refractive Plating buff grants EvasionGainAsDeflection BASE 20 (vendor sup_str.lua:5984/6023, support_tempered_valour_deflection_rating_%_of_evasion_rating). Implementation, following the player-buff stat-map channel: - translate_tag gains a MultiplierThreshold arm (ModStore.lua:429-459). thresholdVar form is admitted only for vars verified to have zero setters vendor-wide (RefractionMinimumValour -> threshold statically 0, matching vendor GetMultiplier-on-unset semantics); vars with real setters (AttritionCullSeconds) stay Unsupported. - player-buff ModName allowlist gains EvasionGainAsDeflection (consumer: calc_deflection, CalcDefence.lua:1516). - pob2-oracle defenceModList name set extended with the deflection family. Parity: wolf-pack DeflectChance 0.80x -> 1.00x, pathfinder 0.93x -> 1.00x; def 25-col baseline ratcheted 405 -> 407 @5% and 428 -> 429 @10% (honest re-measurement, old baselines noted in the constant comments). The same buff's ArmourAppliesTo<El>DamageTaken payloads remain reported (EHP-side lead: wolf-pack TotalEHP 0.76x -> 0.81x).
# Conflicts: # docs/adapting-to-0.5.4b.md
….5.4b #3) The consumption chain already existed end-to-end (calc::taken:: armour_applies_pct -> build_mitigation_ctx -> per-type DamageReduction / MaximumHit / EHP, mirroring CalcDefence.lua:2361-2368), and tree-sourced percentages flow through mod_parser. The only gap was the player-buff stat-map allowlist: Refractive Plating's second stat key (support_tempered_valour_%_armour_to_apply_to_elemental_damage, sup_str.lua:6019-6021, ArmourAppliesTo{Fire,Cold,Lightning}DamageTaken BASE 30 with the same GlobalEffect + MultiplierThreshold tags as the already-admitted deflection payload) hit UnknownModName. Oracle attribution (defenceModList name set extended with the AppliesTo family) pins wolf-pack at tree 84 + buff 30 = 114% per element, <El>EffectiveAppliedArmour = 18580 * 1.14 = 21181.2. Parity: wolf-pack Fire/Cold/LightMaxHit 0.94x -> 0.96x (three new @5% hits) and TotalEHP 0.81x -> 0.88x; no other build moved. def 25-col baseline ratcheted 407 -> 410 @5% (429 @10% unchanged; honest re-measurement, old baselines noted in the constant comments). The EHP remainder is not this channel: Armour itself 0.98x, ChaosMaxHit 0.87x (oracle ChaosEffectiveAppliedArmour = 0), Life 1.11x.
…wLife condition (0.5.4b #4a) 0.5.4b added the LifeReservePercentPerSpirit mechanic (vendor CalcDefence.lua:248-254): Atziri's Communion converts a supported persistent skill's Spirit reservation into a Life percent reservation (constant stat skill_reserves_X_life_permyriad_per_spirit_instead_of_spirit = 66 -> 0.66%/spirit). Heavily-reserved builds then auto-enter Low Life (CalcDefence.lua:335-350: unreserved/max <= LowPoolThreshold 0.35 -> condList["LowLife"]), unlocking the 'while on Low Life' mod family (tree nodes + Direstrike support buff, whose stat-map entry already carried Condition:LowLife but the condition was never set). Three consumption-point fixes, all generic: - xml_build: gems serialized with only nameSpec (lineage supports like Atziri's Communion carry no skillId/gemId) are no longer dropped; GemSkillRef gains name_spec and stage_build_view resolves it against granted_effects by normalized name (PoB2 SkillsTab nameSpec lookup). - spirit_reservation_modifiers: when a group support carries the permyriad stat, the skill's spirit flat (incl. ExtraSpirit) converts to a LifeReservedPercent INC mod (Life-pool inc/more/efficiency names, vendor round-2), spirit reservation zeroed. Eternal Rage: 155 x 0.66 x 0.9 = 92.07% -> LifeReserved 270 (golden 270). - CalculationSession::bridge_low_pool_conditions (orchestrator stage 6e): mirrors the vendor low-pool loop for Life; explicit config conditionLowLife stays authoritative. Oracle evidence (huntress-ritualist-bow-shot, 0.5.4b): damageModList shows +60 (Tree:56453) and +70 (Skill:SupportDirestrikePlayerTwo) attack Damage INC gated on Condition:LowLife = exactly the missing 130 INC (PoBR pool scale 3.48 vs oracle 4.78 across all damage types); mainOutput LifeUnreserved 23 / SpiritUnreserved 10 vs PoBR's old 293 / -130. Parity: ritualist TotalDPS 0.68x->0.99x, TotalDotDPS 0.60x->1.00x, SpiritUnres/LifeUnres exact; abyssal-lich (also wears Communion) TotalDPS 0.62x->0.83x. Aggregates below re-ratcheted in #4b commit.
…ster-socket jewels (0.5.4b #4b)
0.5.4b introduced the Voices unique jewel ('Allocates 2 Sinister Jewel
sockets'; vendor ModParser.lua 0.22.0 delta: GrantedPassive
SinisterJewelSockets, consumed by PassiveSpec.lua:1067-1090 which
allocates the first N sinister sockets in voices_jewel_slot1..5 alias
order). PoBR's tree-jewel gate only admitted jewels whose socket node
is in the allocated set, so jewels sitting in the never-tree-allocated
sinister sockets were silently dropped.
parse_items_and_slots now scans allocated-socket jewels for the
allocates-N line and treats the first N sinister socket node ids
(pinned from vendor TreeData/0_5/tree.lua sinister=true +
aliasPassiveSocket: 62152, 26178, 23960, 39087, 3367) as allocated for
the jewel filter. Older tree versions have no source for this mod, so
behavior there is unchanged.
Oracle evidence: ritualist critModList includes Item:18 Phoenix
Splinter (+18 INC CritMultiplier) and Item:23 Oblivion Sliver (+13 INC
CritChance, +19 INC CritMultiplier) socketed at 62152/26178 - exactly
PoBR's missing 13 INC chance / 37 INC multiplier (27.839->29.1 chance,
4.37->4.74 mult, both now exact). Titan and abyssal-lich also wear
Voices and pick up their sinister jewels.
…anary Communion/LowLife + Voices sinister sockets land honest gains, recorded per ratchet discipline (old -> new, all measured): - offensive @5% 46 -> 52, @10% 55 -> 59 - dot @5% 11 -> 13, @10% 13 -> 18 - defensive 25-col @5% 410 -> 413, @10% 429 -> 434 - defensive core-8 @5% 138 -> 140 - panel offence @5% 27 -> 38, @10% 30 -> 39 canary_fire_spell_armour (druid-oracle-ember-fusillade) re-verified at TotalDPS 1.00x (548742 vs golden 549423) - the gap-map ~0.60x entry had already been closed by earlier fixes without the canary being revisited; un-ignored.
ORACLE_EXTRA_STATS="CooldownRecovery,Speed" dumps the per-source mod list for arbitrary vendor stat names (same shape as critModList) under extraModList. Zero cost when unset. Used to pin the deadeye grenade Speed gap to three tree nodes (21077/354/48429, 15% CDR each, SkillType 159-gated) without hand-bisecting the build XML.
… (0.5.4b #4c) vendor 0.22.0 added 'not grantedEffect.fromItem' to the ModParser gem- name registration loop (ModParser.lua:6423, in the 0.21->0.22 delta): MeleeGrenadeLauncherPlayer (name "Grenade", fromItem) no longer registers in skillNameList, so the modFlagList phrases 'grenade' and 'for grenade skills' are live SkillType.Grenade tags again. PoBR's extractor still carried the 0.21-era runtime facts from PR#53 (VENDOR_DEAD_FLAG_PHRASES dropped 'for grenade skills'; VENDOR_SKILLNAME_PREEMPTED rewrote 'grenade' to an inert SkillName tag), so grenade-scoped mods silently under-applied. Evidence: - run-parsemod (0.5.4b vendor): '15% increased Cooldown Recovery Rate for Grenade Skills' -> CooldownRecovery INC 15 + SkillType 159, empty leftover; '12% increased Grenade Damage' likewise. - oracle extraModList: deadeye is missing exactly Tree:21077/354/48429 (3 x 15 CDR, SkillType-gated) -> INC -18 vs vendor +27, cooldown 6.098s vs 3.937s (Speed 0.164 vs golden 0.254). Changes: empty both extractor override tables (kept with a dated note and the re-audit method), regen mod_parser_rules.json (exactly the two grenade entries change) + parsed_mods.json + test_pins snapshot. Parity: deadeye Speed 0.65x->1.00x, TotalDPS 0.53x->0.83x (remaining gap is the pre-existing per-hit shortfall documented in pob2_parity.rs); gemling Speed/AvgDamage/TotalDPS/CombinedDPS all 1.00-1.04x. Ratchets: off @5% 52->56, @10% 59->62; dot 13->14 / 18->19; panel 38->40 / 39->41. The stale embedded deadeye sample (pre-0.5.4b export) no longer gets a TotalDPS assertion - its Speed calibration is invalidated by the CDR rebalance; AverageDamage tightened 0.55->0.20 (measured 1.04x). The authoritative DPS gate is the ninja_parity 0.5.4b fixture golden.
Adds the #4 section (three sub-fixes: Communion/LowLife chain, Voices sinister sockets, grenade phrase un-preemption), the honest aggregate deltas, and the decomposed remaining offence clusters (attack AverageDamage family, titan bifurcated-crit, frost-bomb pre-existing cooldown gap, ritualist low-life EHP calibre note). Includes cargo fmt fixups on the #4a code.
…0.5.4b #5) Vendor 0.22.0 added a GlobalEffect/Buff tag to Blazing Critical's support_blazing_crits_gain_%_fire_damage_with_attacks_on_critical_hit (sup_int.lua:959), turning its 15% DamageGainAsFire (Attack + Condition:CritRecently) from a dead skill-local mod into a global player buff. Two generic consumption points: - stat_map_engine: admit DamageGainAsFire in the player-buff mod-name allowlist (consumer = the existing gain-as matrix in calc::damage). - support_buff_specs: judge support compatibility against additional granted effects too (mirrors buff_skill_specs) -- the compatible host for Blazing Critical under Charged Staff is the hidden Attack additional effect ChargedStaffShockwavePlayer (act_int.lua:3387). Ignite scales quadratically with the fire portion (chance ~ fire/ threshold, magnitude ~ fire): flicker TotalDotDPS 0.05x->0.72x, spirit-walker 0.23x->0.87x, monk-twister 0.44x->0.98x (its AverageDamage/TotalDPS also 0.60x->0.96x).
off 56->58 @5% (62->64 @10%), dot 14->16 @5% (19->21 @10%); def/panel unchanged (CritRecently is combat-gated). Per-cell triage of the remaining dot misses recorded at the constants: all leftovers are either squared pass-through of the out-of-scope attack AvgDamage hit gaps or pre-0.5.4b items (smith Infernal Cry uptime gain, frost-bomb, essence-drain WithDotDPS).
…urce vendor Item.lua:1923-1928 folds every bare 'Adds N to M <type> Damage' line on a weapon into weaponData (local: it only fires when attacking with that weapon). PoBR only treated the physical variant as local; elemental/chaos lines leaked into the global added-damage bucket and got multiplied by the skill's added-damage effectiveness even for skills that never use the weapon. warrior-titan-shield-wall: Nebuloch's 'Adds 30 to 52 Chaos damage' became a phantom 104-180 chaos base on Shield Wall (oracle ChaosMinBase 0) => TotalDPS 1.05x; warrior-smith-of-kitava: Fortified Hammer's 'Adds 3 to 55 Lightning' likewise => AverageDamage 1.02x. The overestimate was previously masked by the missing warcry gain. inject_items now drops bare typed adds (all five types) from a weapon slot whose contribution is not consumed by the main skill (non-weapon attacks like Shield Wall, spells, inactive off-hand). Active weapon sources keep the existing global-injection approximation (numerically equal to the vendor weaponData fold; deadeye/twister pinned at 1.00x, druid spell unaffected -- its spear's bare adds are attack-gated and never reached the spell). titan TotalDPS 1.05x -> 1.01x, smith AverageDamage 1.02x -> 1.00x. Remaining titan ~0.9% decomposes into the known shield-armour recompute divergence (rolled 'Armour: 1736' vs vendor recompute 1725 feeding Shield Wall's per-15-shield-armour phys base; same root as the Armour 0.99x panel row) plus crit-chance rounding (11.28 vs 11.2476) -- defence-side channel, not an offence bug.
- Baselines re-pinned to the post-#10 dashboard: offensive 76->78/80 @5% and @10% (deadeye AverageDamage+TotalDPS enter the band), dot 31->34 @5% / 34->36 @10% (deadeye ignite chain follows the Point Blank fix; smith/titan dot back in band after the local-adds leak removal). - POBR_ONLY_BUILD=<substring> filter for the ninja dashboard so POBR_DBG_* channels stay readable on a single build (never set in CI). - POBR_DBG_BASES now also dumps generic-bucket MORE contributions (POBR_POOL_MORE), symmetric with the existing INC dump.
# Conflicts: # crates/pobr-build/tests/parity/ninja_parity.rs
…evel Vendor uses monsterAllyLifeTable for non-hostile minions (CalcPerform.lua:1046, floor semantics), not the enemy monsterLifeTable PoBR was reading. Promote the ally table to a Rust const (MONSTER_ALLY_LIFE_TABLE, deduping the literal previously inlined in the catalog Default impl) and switch derive_minion_base_stats to it. Oracle pin (wolf-pack): ally[44]=2938 x minionData.life 1.1 = 3231 base life, matching the vendor Life BASE mod exactly.
…-list Two narrow statmap channels for the companion defence layer (#12): - map_minion_life_stat: translate 'MinionModifier LIST { mod = Life ... }' statmap payloads (e.g. Loyalty's support_trusty_companion_minion_life_+%_final -> Life MORE -30) into minion-side inner modifiers, normalising vendor 'Life' to the engine life-pool name 'MaximumLife'. First batch admits inner Life only (BASE/INC/MORE); anything else is skipped, never mis-applied. - admit TakenFromCompanionBeforeYou through the player-buff allow-list (Loyalty / Wild Protector carry companion_takes_%_damage_before_you_from_support with GlobalEffect/Buff/unscalable, SkillStatMap.lua:2559-2561).
Implements the vendor companion allies layer (CalcDefence.lua:2961-2965 mitigation, :493-495/:525-531 EHP drain, :3656-3663 max-hit poolProtected fold; CalcPerform.lua:3323-3370 life pool): - Actor::is_companion marks damageable-companion minions at spawn (granting skill has SkillType.Companion and not MinionsAreUndamagable); add_minion_from_def/session wrapper take the flag. - spawn_minions now feeds summons the effective gem level (+N to Level of ... bonuses + support-granted levels, vendor minionLevelTable[activeEffect.level]) and appends group-scoped support minion payloads via map_minion_life_stat (Loyalty -30% more life). - perform runs perform_minions before fill_mechanics (vendor computes minion life pools before player defence) and inject_companion_life writes TotalCompanionLife BASE into the player ModDb (Override-aware, vendor CalcPerform.lua:3364-3370). - build_pool_state emits the 'companion' AllyLayer consumed by the existing shared EHP/max-hit allies machinery. The deflected-hits term (TakenFromCompanionBeforeYouFromDeflected x DeflectChance) is not modelled: no source in the 18-build corpus and the wolf-pack oracle pins mitigation at exactly 10 (pure hits term). Design note: no companion actor abstraction - the existing minion actor plus the generic AllyLayer machinery express the whole mechanic; the only new state is one bool on Actor and one BASE mod on the player db. Oracle pins (mercenary-tactician-wolf-pack): TotalCompanionLife 2262 = floor(ally[44] 2938 x 1.1) x 0.7; CompanionAllyDamageMitigation 10; <Type>TotalHitPool 3826.67 (PoBR now 3817.0 = 0.9975, remainder is the pre-existing Mana 761.2-vs-770 gap). Parity: huntress-spirit-walker-twister closes exactly (5x MaxHit + TotalEHP 0.89-0.90x -> 1.00x); wolf-pack MaxHit 0.82->0.90x, ChaosMaxHit 0.72->0.80x, TotalEHP 0.74->0.83x (residual is a uniform per-type taken-multiplier gap, not the companion layer). Baselines ratcheted: def 425->431 @5%, 434->439 @10%; only these two builds moved in the 18-build per-cell diff.
…or parity) Vendor's <X>TakenHit chain (CalcDefence.lua:2402) uses calcs.armourReduction — the *integer-rounding* variant (Common.lua round = floor(x+0.5)) — while only takenHitFromDamage (:437) uses the fractional armourReductionF. PoBR's taken_hit_per_type used the fractional value everywhere, so the exported PhysicalDamageReduction never matched PoB2's always-integer golden at small magnitudes. druid-oracle-ember-fusillade PhysDR 4.23 -> 4.00 (1.06x -> 1.00x) and ranger-deadeye-explosive-grenade PhysDR 2.39 -> 2.00 (1.20x -> 1.00x). Def 25-col 431 -> 433 @5%, 439 -> 440 @10%.
Vendor doActorLifeManaSpirit (CalcDefence.lua:92-95) deducts
min(Sum(BASE, res..ConvertTo{EnergyShield,Armour,Evasion}), 100) percent
from the Life/Mana pool base. PoBR's defence matrix already credited the
receiving side (ES gains ceil(base*rate/100), :1364) but the pool-side
deduction was explicitly deferred and never landed, so builds with
'N% of Maximum Life Converted to Energy Shield' kept full Life.
witch-abyssal-lich-detonate-dead allocates 0_5-tree 'Enhanced Barrier'
(node 44299, 5% Life -> ES; oracle extraModList pins
LifeConvertToEnergyShield BASE 5 from Tree:44299): Life 1782.98 -> 1693.83
vs golden 1694 (1.05x -> 1.00x), LifeUnreserved follows. Def 25-col
433 -> 435 @5%, core-8 142 -> 143.
…le round 0.5.4b vendor (CalcDefence.lua:229-239) adds blasphemy_base_spirit_reservation_per_socketed_curse x instances into pool.Spirit.baseFlat *before* the one efficiency-scaled round; PoBR still used the old per-instance round-then-multiply (round(60/1.1)=55 x3 = 165 vs vendor round(180/1.1) = 164, oracle spiritReservedBreakdown pins 164). sorceress-chronomancer-essence-drain SpiritUnreserved 6 -> 7 (exact). Def 25-col 435 -> 436 @5%, 441 @10%. Skills-suite pin updated 110 -> 109.
Vendor gems carry a second quality-stat table (altQualityStats) that only applies when the build has 'Gem Quality grants Socketed Skills an additional effect' (Gemling ascendancy notable -> GemlingQuality flag, ModParser.lua:3353 / CalcSetup.lua:835 / CalcTools.lua:147-152). The extract-lua gem-quality channel never transcribed those rows, so gemling lost e.g. Mirage Archer's alt base_reservation_efficiency_+% x2 and Eternal Rage's alt base_spirit_reservation_efficiency_+% x0.75. - extractor: emit altQualityStats rows with alt:true; QualityStat gains a serde-default alt flag (old data unchanged); regenerated data/4.5.4.3/overlay/gem_quality_stats.json. - BuildData::effect_stats/unselected_set_stats exclude alt rows (byte-identical behavior for every existing caller); new alt_quality_stats accessor returns trunc(rate x quality) alt values. - spirit reservation consumer: detect the flag from allocated/anointed nodes (gemling_quality_flag) and fold alt efficiency stats in; also admit the spirit-specific base_spirit_reservation_efficiency_+% id. mercenary-gemling-legionnaire SpiritUnreserved -60 -> -18 (exact; oracle pins Mirage Archer 60->37 eff 62%, Eternal Rage 100->81 eff 23% at effective quality 31). Def 25-col 436 -> 437 @5%, 441 -> 442 @10%.
…tion What changed - setup_enemy injects the vendor actor-init conditional pair on the enemy modDB: Damage INC -10 / DamageTaken INC 10 gated on EnemyIntimidated (CalcSetup.lua:73-77). - env_finalize stage 7.5 bridges an enemy-db Condition:Intimidated FLAG (e.g. item mod "Enemies in your Presence are Intimidated") into cfg EnemyIntimidated (vendor ModStore GetCondition semantics; allow-list = Intimidated only). - Orchestrator seeds EnemyInPresence true (CalcPerform.lua:524 default; PresenceRadius vs enemyDistance not modelled). Why - wolf-pack's uniform ~10% per-type taken-multiplier gap: oracle pins <X>EnemyDamageMult = 0.9 from the Intimidated base mod lit by the body armour's corrupted implicit; PoBR had the flag in the enemy db but no consumer. The max-hit end divisor and EHP damage-in multiplier were already wired - only the mod was missing. Validation - wolf-pack Phys/Fire/Cold/Light MaxHit 0.90x -> 1.00x, PhysDR 65.69 -> 68.03 (oracle 68), TotalEHP 0.83x -> 0.98x. No other build's cells move (per-cell 18-build diff).
What changed - pobr_data::Item gains a corrupted flag, parsed from the item text 'Corrupted' marker line in both item_text parse paths (struct literal sites updated with corrupted: false). - stage_inject_jewels: when a unique jewel carries 'N% increased Effect of Jewel Socket Passive Skills containing Corrupted Magic Jewels' (The Adorned; the text wraps across two physical lines in build XML), every corrupted MAGIC tree-socketed jewel's mods are parsed and value-scaled by 1 + N/100 before injection, using vendor ScaleAddList rounding trunc(round(v * scale, 2)) (ModStore.lua:70-79; CalcSetup.lua:944-948 + :1342-1347). Why - wolf-pack Mana 761.2 vs 770: six corrupted magic 'Rallying Ruby of Valour' jewels (+Int/+Dex/+chaos res enchants) must scale x1.97 (oracle: +5 Int evaluates to 9, +4 Dex to 7). Int 135 -> 139 closes Mana exactly; uncapped chaos resist 72 -> 81 closes the ChaosMaxHit tail. Not modelled (no corpus source): sinister/containJewelSocket socket exemptions, unscalable payloads. Validation - wolf-pack Mana/ManaUnres 770.00 exact, ChaosMaxHit 17008 vs 17007, TotalEHP 129929 vs 130326 (1.00x).
What changed - item_rolled_defence: when the base item is known, all three defence components (armour/evasion/energy shield) are recomputed as round((base + localFlat) * (1 + localInc/100) * (1 + quality/100)) (Item.lua:1994-1996), instead of ES only. Display-line fallback remains for unknown bases. Why - titan Armour 0.985x: three 0.5.4b-buffed Runeforged bases whose item-text 'Armour:' lines lag the base DB (gloves 96 vs 101, helmet 192 vs 284, boots 58 vs 100; oracle ArmourOn<Slot> pins each; helmet recompute 169*1.40*1.20 = 284 exact). Same family as the earlier ES-only fix (c705fe4). Note the #10 'shield 1736 vs 1725' hypothesis was wrong - oracle armourData.Armour is exactly 1736. Validation - titan Armour 39545.68 vs 39546 (0.99x -> 1.00x), ES/ESRecoveryCap 55 exact, Phys/Fire/Cold/Light/Chaos MaxHit exact. Per-item round() also lands pathfinder Evasion 0.98x -> 1.00x and twister DeflectChance 0.97x -> 1.00x exact. No cell regresses fleet-wide.
- enemyMitigation.dealtMods: per-mod source dump of enemy outgoing-Damage scalers (INC/MORE over Damage/<Type>Damage/ElementalDamage) - the feeds of <X>EnemyDamageMult (Enfeeble / Intimidated-style). - ORACLE_DUMP_ITEMS="2,17": dumps listed build items' parsed modLines, evaluated modList, quality, armourData and base armour - provenance diffs for jewel-effect scaling and item defence recompute. Zero cost when unset.
- def 25-col: 431 -> 437 @5%, 439 -> 445 @10% (six wolf-pack cells: 5x MaxHit + TotalEHP). off/dot/core-8 unchanged. - docs/adapting-to-0.5.4b.md: #13 section - three root causes (enemy Intimidated base pair, The Adorned corrupted-magic-jewel scaling, Runeforged base-armour display-line lag) with oracle evidence and vendor line refs; oracle tooling notes.
The #12 companion work switched summon life to monsterAllyLifeTable with vendor floor semantics (CalcPerform.lua:1046); two minion tests still pinned the enemy monsterLifeTable expectation and slipped through the merge gate (the pobr-core run output was truncated by the tee log).
…quirement snapshot
Two provisioning gaps left Masterwork's armour mods evaluating to zero on
warrior-smith-of-kitava-shield-wall (both mods parsed fine, their inputs
were never provided):
1. Multiplier:AllocatedConnectedNotable — vendor counts allocated notables
flagged applyToArmour=true in tree.lua (PassiveSpec.lua:1753,
CalcSetup.lua:840-841). The flag never existed in PoBR tree data: it is
now transcribed by the --tree-coords backfill (PassiveNodeDef.apply_to_armour,
serde-default) and counted in inject_per_x_multipliers. Smith allocates
8 of the 12 flagged notables -> +200x8 = 1600 flat Armour.
2. StrRequirementsOn<slot> — vendor snapshots per-slot item attribute
requirements (CalcPerform.lua:1848-1857) for 'Gain Armour equal to 150%
of total Strength Requirements of Equipped Boots, Gloves and Helmet'
(PercentStat). Base-item requirements were absent from PoBR data (.dat
bundle unavailable) — extended the extract-bases vendor channel with
req = { str/dex/int } (BaseItemDef.req_str/dex/int via overlay merge)
and provision <Attr>RequirementsOn<slot> cfg.stats per equipped item.
reqMult (GlobalAttributeRequirements) held at 1 until a fixture needs it.
smith Armour 37386 -> 49096.16 vs golden 49096 (0.76x -> 1.00x exact);
PhysMaxHit/Fire/Cold/LightMaxHit all land exact (0.91-0.93x -> 1.00x),
ChaosMaxHit 16124 vs 16123. Def 25-col 437 -> 442 @5% / 442 -> 444 @10%,
core-8 143 -> 144/144 (100%).
Vendor's EHP mitigation layer uses EffectiveAverageBlockChance = mean of (attack, projectile, spell, spell-projectile) block (CalcDefence.lua:1067), where SpellProjectileBlockChance = max(spellBlock+proj part, ProjectileBlock) (:1013). PoBR averaged only (attack + spell)/2, so shield builds with zero spell block were undercounted: smith 13.65% vs vendor 20.475%. calc_block already computed all four raw categories — exposed the two effective projectile variants through BlockResult/OutputTable and switched the EHP average to the 4-way mean. warrior-smith-of-kitava TotalEHP 91338 -> 99177 vs golden 99167.69 (0.92x -> 1.00x) and warrior-titan TotalEHP 54483 -> 58898 vs 58904.30 (0.92x -> 1.00x). Def 25-col 442 -> 444 @5% / 444 @10%.
# Conflicts: # crates/pobr-build/tests/parity/ninja_parity.rs # docs/adapting-to-0.5.4b.md
…nodes Reverts the 75a348e mechanism (inactive weapon-set nodes merged back into the radius-jewel grant geometry). That change was based on a misreading: in vendor, EVERY mod on an allocMode != 0 node - the node's own stats and radius-jewel grants alike - gets 'Condition: WeaponSet<N>' appended (CalcSetup.lua:222-223; the jewel-source branch :224-227 only fires for allocMode-0 nodes), so grants on inactive-set notables are dead. Oracle critModList Tabulate on gemling pins exactly one '7 @ Tree:32763' (INC total 71 -> CritChance 5 x 1.71 = 8.55, matching the 0.5.4b golden). PoBR previously granted 6x +7 -> 10.30 (1.20x overshoot). gemling: CritChance 1.20x -> 1.00x exact; the same jewel's Small-node grants shrink too -> AvgDamage/TotalDPS 1.04x -> 0.97x (in band); TotalDotDPS 1.10x -> 0.95x (in band; residual is purely downstream of the pre-existing AvgDamage under-estimate via ignite chance x stack potential, 0.9754^2 = 0.951 exact). Other 17 builds unaffected.
…kills only eat SkillSpeed wolf-pack Speed read 1.43 vs vendor 1.00. Two stacked causes: 1. Main-skill selection: Wolf Pack (Minion+Companion, neither Attack nor Spell) failed the is-damage-skill filter, so mainSocketGroup=4 yielded nothing and the fallback scan hijacked the main skill to the Blasphemy group's Temporal Chains (0.7s cast -> Speed 1.43). Vendor's socketGroupSkillList has no damage filter. Fix: when pick_group_main_skill misses on the DESIGNATED group, fall back to the mainActiveSkill-chosen active gem (pick_group_chosen_active; Meta shells still excluded, cross-group scan still damage-only). 2. Speed bucket: with Wolf Pack as main (castTime 1s), the 'untyped skill eats all three speed buckets' fallback consumed the weapon's '12% reduced Attack Speed' -> 0.88. Vendor mod-matching requires cfg to carry ModFlag.Attack/Cast for those mods; untyped skills eat neither. speed_names_for now returns only SkillSpeed for non-attack/non-spell. Oracle mainOutput pins Speed=1, Time=1, CastRate=1 (statSet 'Minion Info', player TotalDPS 0). wolf-pack Speed now 1.00x; all other builds' main skills are attack/spell typed - no behavior change. Also adds a POBR_DBG_SPEED probe (main skill id + speed factor dump).
…ecord #15 Offensive 78->80 (gemling CritChance, wolf-pack Speed), dot 36->37 (gemling TotalDotDPS back in band), panel offensive ratchet 42/43 -> 45/47. def stays 450/450, core-8 144/144. All 18-build cells now hit at 5% - zero per-cell regressions by construction. Documents #15 root causes in docs/adapting-to-0.5.4b.md and corrects the stale 75a348e comment (its 6x+7 claim matched the pre-0.5.4b golden; current oracle/golden pin 1x+7 = 8.55).
… with #7 memory_backend_matches_dir_backend only staged the version directory, so the P1-3 overlay-common layer was silently absent from the in-memory backend (production wasm bundles it via web sync-data). Stage it under the overlay-common/ key prefix the loader resolves. Two unsupported_specials tests still pinned the pre-#7 also_grants_guard phantom modeling; guard lines now assert loud unsupported reporting and no hit-pool extension, matching PoB2. The wasm suite was in no wave's merge gate, which is how both slipped through.
ackness
marked this pull request as ready for review
July 18, 2026 15:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
The v0.0.1→v0.0.2 release diff was +101k/-25k lines, perceived as "10k+ lines of core code just to bump the game version". Dissection (
docs/version-bump-architecture.md) showed ~92k was tool-generated JSON and only ~2-3k was human-written bump work — but that residue was real recurring toil. This branch eliminates the self-inflicted part, shrinks the discovery cost of the intrinsic part, and then uses the new tooling to finish the 0.5.4b engine adaptation itself.Bump-cost architecture (the five planned items)
pobr_gamedata::test_pins): data-content count/float assertions move todata/<ver>/generated/test_pins.json, refreshed byPOBR_BLESS_PINS=1as regen-all's final step. Coverage baseline re-classified as a ratchet (direction assertion, manual raise).pipeline/diff-vendor-calcs.sh): diffs two PoB2 vendor pins into a markdown triage list. Sample report for 0.21.0→0.22.0 committed (devs/docs/audits/vendor-delta-ce8bffab.md).data/overlay-common/: all five hand-curated overlay domains (special_mods + buff_definitions + high_precision_mods + local_mods + vendor_name_aliases) now live version-independently and are inherited by every data version; per-version hand-porting is gone.pipeline/bump-version.sh: the whole bump drill as one idempotent command; golden flip / vendor pin choice / engine adaptation stay explicit manual decisions..gitattributes: generated artifacts markedlinguist-generated -diff.0.5.4b engine adaptation finished with the new tooling (gap-map #2–#5)
Data versioning, tree diagnostics, FullDPS, and M6 parser cleanup #2 DeflectionRating: re-triage showed the 0.60x entry was downstream of the already-fixed Evasion gap; the genuine gap was the Refraction support buff (
EvasionGainAsDeflection), admitted with a conservativeMultiplierThresholdtag arm. wolf-pack DeflectChance 0.80x→1.00x.Data versioning, tree diagnostics, FullDPS, and M6 parser cleanup (audit-fixed; supersedes #2) #3 ArmourAppliesTo<El>DamageTaken: consumption chain already existed; three-name buff allowlist closes wolf-pack elemental MaxHits to @5% and TotalEHP 0.81x→0.88x.
ci: add GitHub Actions workflow (fmt + clippy + nextest) #4 offence DPS clusters: three root causes — Atziri's Communion spirit→life reservation + auto LowLife bridge, Voices sinister jewel sockets, and the 0.22 grenade-phrase re-enable (pointed at directly by the P0-2 delta report hunk). ritualist 0.68x→0.99x, gemling fully closed.
M6 prep: engine parity fixes + newAura wrapping + aux ingest migration #5 ailment magnitude: constants unchanged — the real cause was Blazing Critical's 15% DamageGainAsFire becoming a global buff in 0.22. monk-twister dot 0.44x→0.98x.
buff_definitions vendor_ref audit: all 15 drifts adjudicated cosmetic (hashes were stale since pre-0.21.0); refs re-pointed to 0.22.0,
check-buff-refsnow 0 DRIFT.Backlog sweep + residual waves (feat(m6): stat_id → Modifier second channel (extraction + generator) #6–fix(parity): stop over-parsing 'Grants Onslaught during effect' (phantom +20% speed) #15): AvgDamage family (Bifurcate crit pipeline, DistanceRamp/Point Blank, Barrage repeats channel, weapon-adds leakage), Communion low-life chain, named jewel socket via anoint, Blasphemy meta-gem support halves, warcry uptime machine, RemoveStats data-pipeline column, phantom Guard de-modeling, companion ally-mitigation layer (no actor abstraction), enemy Intimidated base pair, The Adorned corrupted-jewel scaling, base-DB defence recompute, altQualityStats channel, connected-notable armour multiplier, four-category block averaging, jewel×weapon-set gating revert, and main-skill selection for Minion+Companion skills.
Final parity: all four panels at their ceiling — def 25-col 450/450 = 100%, def core-8 144/144 = 100%, offence 80/80 = 100%, dot 37/37 = 100% (both @5% and @10%), up from the Phase-0 migration baseline of 343/118/39/9. Every cell of all 18 real poe.ninja builds now matches PoB2.
How it was validated
parity_no_regressionratchets), dualrun, skills, pobr-gamedata, precompile-mods, clippy-D warnings,cargo fmt --check.docs/adapting-to-0.5.4b.md.Known risks / follow-up