fix(player): apply AttributeFormula to wire-derived Run/Jump skill — root cause of short jumps
Found the underlying cause of the user's persistent "jumps don't reach retail height" complaint. The wire's SkillEntry `init` field is ONLY the InitLevel (training/specialized chargen bonus, per ACE GameEventPlayerDescription.cs:317 "init_level, for training/specialized bonus from character creation"). It does NOT include the AttributeFormula contribution. ACE's CreatureSkill.Current is computed as: AttributeFormula(skill, attrs) + InitLevel + Ranks + augs + multipliers - vitae Pre-fix13 we used `init + ranks` only — dropping the AttributeFormula term, which is the DOMINANT component for movement skills (50-100 points typical). For our character that meant Jump skill 208 instead of the actual ~280-310, giving a 3.11 m peak instead of the retail ~4 m peak. Hence "feels like the upward acceleration is too slow and we don't reach the same height". Fix: - GameWindow caches portal.dat's SkillTable (0x0E000004u) at WireAll time. Each entry has a SkillFormula with attr1/ attr2/multipliers/divisor/additive constants (formula: bonus = (attr1*M1 + attr2*M2)/Div + Additive). - GameEventWiring.WireAll gains a `resolveSkillFormulaBonus(skillId, attrCurrents)` callback. GameWindow plugs in a resolver that looks up SkillTable.Skills[skillId].Formula, applies the formula using the player's current attribute values from PD. - The PD handler builds attrId→current map (ranks+start) from the parsed attributes before iterating skills, then passes it to the resolver for Run (24) and Jump (22). - Total skill = formulaBonus + InitLevel + Ranks. Matches ACE Current minus augs/multipliers/vitae (close enough — those add maybe ±10 % at most). ACDREAM_DUMP_VITALS=1 logs add a per-skill line: "vitals: PD-skill id=22 init=N ranks=N formulaBonus=N total=N" so live testing can confirm the formula is applied. Tests stay 1222 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4b6fcffa01
commit
a060f4fc98
2 changed files with 83 additions and 11 deletions
|
|
@ -1220,9 +1220,37 @@ public sealed class GameWindow : IDisposable
|
||||||
// this one call, server-sent ChannelBroadcast / damage
|
// this one call, server-sent ChannelBroadcast / damage
|
||||||
// notifications / spell learns / wield events all update
|
// notifications / spell learns / wield events all update
|
||||||
// the corresponding client-side state without further glue.
|
// the corresponding client-side state without further glue.
|
||||||
|
// K-fix13 (2026-04-26): cache portal.dat's SkillTable so the
|
||||||
|
// skill-formula resolver can apply the AttributeFormula
|
||||||
|
// contribution. Without this, our wire-derived "skill"
|
||||||
|
// value was missing the dominant attribute-derived term
|
||||||
|
// and jumps undershot retail by ~30 % at typical
|
||||||
|
// attribute levels.
|
||||||
|
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
|
||||||
|
|
||||||
AcDream.Core.Net.GameEventWiring.WireAll(
|
AcDream.Core.Net.GameEventWiring.WireAll(
|
||||||
_liveSession.GameEvents, Items, Combat, SpellBook, Chat, LocalPlayer,
|
_liveSession.GameEvents, Items, Combat, SpellBook, Chat, LocalPlayer,
|
||||||
TurbineChat,
|
TurbineChat,
|
||||||
|
resolveSkillFormulaBonus: (skillId, attrCurrents) =>
|
||||||
|
{
|
||||||
|
// ACE GetFormula (AttributeFormula.cs:55-): when
|
||||||
|
// formula.X (Attribute1Multiplier) is 0, the formula
|
||||||
|
// is "no attribute contribution" and the function
|
||||||
|
// returns 0. Otherwise:
|
||||||
|
// bonus = (attr1 * Mult1 + attr2 * Mult2) / Divisor + Additive
|
||||||
|
if (skillTable?.Skills is null) return 0u;
|
||||||
|
if (!skillTable.Skills.TryGetValue(
|
||||||
|
(DatReaderWriter.Enums.SkillId)skillId, out var skillBase))
|
||||||
|
return 0u;
|
||||||
|
var f = skillBase.Formula;
|
||||||
|
if (f.Attribute1Multiplier == 0 || f.Divisor == 0) return 0u;
|
||||||
|
attrCurrents.TryGetValue((uint)f.Attribute1, out uint a1);
|
||||||
|
attrCurrents.TryGetValue((uint)f.Attribute2, out uint a2);
|
||||||
|
long num = (long)a1 * f.Attribute1Multiplier
|
||||||
|
+ (long)a2 * f.Attribute2Multiplier;
|
||||||
|
long bonus = num / f.Divisor + f.AdditiveBonus;
|
||||||
|
return bonus < 0 ? 0u : (uint)bonus;
|
||||||
|
},
|
||||||
onSkillsUpdated: (runSkill, jumpSkill) =>
|
onSkillsUpdated: (runSkill, jumpSkill) =>
|
||||||
{
|
{
|
||||||
// K-fix7 (2026-04-26): cache the latest server-sent
|
// K-fix7 (2026-04-26): cache the latest server-sent
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,25 @@ public static class GameEventWiring
|
||||||
// or Jump (22) entries — we only care about those two for
|
// or Jump (22) entries — we only care about those two for
|
||||||
// movement physics. Caller (GameWindow) plumbs this into the
|
// movement physics. Caller (GameWindow) plumbs this into the
|
||||||
// active PlayerMovementController + caches it for the next
|
// active PlayerMovementController + caches it for the next
|
||||||
// EnterPlayerModeNow construction. Each value is `init + ranks`
|
// EnterPlayerModeNow construction.
|
||||||
// (the holtburger-named "init" field is already the
|
//
|
||||||
// attribute-derived initial component, ranks is XP-bought
|
// K-fix13 (2026-04-26): the wire's `init` field is ONLY
|
||||||
// additions; matches retail's GetCreatureSkill.Current minus
|
// InitLevel (training tier from chargen, per ACE
|
||||||
// augs / multipliers / vitae).
|
// GameEventPlayerDescription.cs:317 "init_level, for
|
||||||
Action<int /*runSkill*/, int /*jumpSkill*/>? onSkillsUpdated = null)
|
// training/specialized bonus"). The AttributeFormula
|
||||||
|
// contribution (Strength/Quickness/etc-derived) is computed
|
||||||
|
// by ACE at runtime via portal.dat's SkillTable + attribute
|
||||||
|
// currents. Without it our totals undershoot the real
|
||||||
|
// Current skill by 50-100 points for movement skills, which
|
||||||
|
// is why jumps looked too short. The optional
|
||||||
|
// <c>resolveSkillFormulaBonus</c> callback lets the caller
|
||||||
|
// (GameWindow) plug in the AttributeFormula contribution
|
||||||
|
// using its cached SkillTable + attribute currents — when
|
||||||
|
// present, total skill = formulaBonus + init + ranks
|
||||||
|
// (matching ACE's CreatureSkill.Current minus
|
||||||
|
// augs/multipliers/vitae which we still don't model).
|
||||||
|
Action<int /*runSkill*/, int /*jumpSkill*/>? onSkillsUpdated = null,
|
||||||
|
Func<uint /*skillId*/, IReadOnlyDictionary<uint, uint> /*attrCurrents*/, uint /*formulaBonus*/>? resolveSkillFormulaBonus = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||||
ArgumentNullException.ThrowIfNull(items);
|
ArgumentNullException.ThrowIfNull(items);
|
||||||
|
|
@ -271,6 +284,23 @@ public static class GameEventWiring
|
||||||
Console.WriteLine($"vitals: PlayerDescription body.len={e.Payload.Length} parsed={(p is null ? "NULL" : $"vec={p.Value.VectorFlags} attrs={p.Value.Attributes.Count} spells={p.Value.Spells.Count}")}");
|
Console.WriteLine($"vitals: PlayerDescription body.len={e.Payload.Length} parsed={(p is null ? "NULL" : $"vec={p.Value.VectorFlags} attrs={p.Value.Attributes.Count} spells={p.Value.Spells.Count}")}");
|
||||||
if (p is null) return;
|
if (p is null) return;
|
||||||
|
|
||||||
|
// K-fix13 (2026-04-26): build attrId → current map while
|
||||||
|
// iterating attributes so the skill-formula resolver below
|
||||||
|
// can apply (attr1.current * mult1 + attr2.current * mult2)
|
||||||
|
// / divisor + additive. "current" here = ranks + start
|
||||||
|
// (the formula-relevant attribute level pre-augs / pre-buffs).
|
||||||
|
// Built unconditionally (not inside the localPlayer guard)
|
||||||
|
// because the skill-formula resolver needs it even if no
|
||||||
|
// LocalPlayerState is wired.
|
||||||
|
var attrCurrents = new Dictionary<uint, uint>();
|
||||||
|
foreach (var attr in p.Value.Attributes)
|
||||||
|
{
|
||||||
|
// PD-attr ids 1-6 are primary attributes (Str / End
|
||||||
|
// / Coord / Quick / Focus / Self). 7/8/9 are vitals.
|
||||||
|
if (attr.AtType >= 1 && attr.AtType <= 6)
|
||||||
|
attrCurrents[attr.AtType] = attr.Ranks + attr.Start;
|
||||||
|
}
|
||||||
|
|
||||||
if (localPlayer is not null)
|
if (localPlayer is not null)
|
||||||
{
|
{
|
||||||
foreach (var attr in p.Value.Attributes)
|
foreach (var attr in p.Value.Attributes)
|
||||||
|
|
@ -321,16 +351,30 @@ public static class GameEventWiring
|
||||||
int jumpSkill = -1;
|
int jumpSkill = -1;
|
||||||
foreach (var s in p.Value.Skills)
|
foreach (var s in p.Value.Skills)
|
||||||
{
|
{
|
||||||
int total = (int)(s.Init + s.Ranks);
|
if (s.SkillId != 22u && s.SkillId != 24u) continue;
|
||||||
|
|
||||||
|
// K-fix13: total = AttributeFormula(skill, attrs)
|
||||||
|
// + InitLevel (s.Init from wire)
|
||||||
|
// + Ranks (s.Ranks from wire)
|
||||||
|
// matches ACE CreatureSkill.Current minus
|
||||||
|
// augs/multipliers/vitae. The attribute-formula
|
||||||
|
// contribution is the dominant term for movement
|
||||||
|
// skills (typically 50-100 points) and was being
|
||||||
|
// dropped pre-fix13 — that's the root cause of
|
||||||
|
// jumps being too short relative to retail.
|
||||||
|
uint formulaBonus = resolveSkillFormulaBonus is not null
|
||||||
|
? resolveSkillFormulaBonus(s.SkillId, attrCurrents)
|
||||||
|
: 0u;
|
||||||
|
int total = (int)(formulaBonus + s.Init + s.Ranks);
|
||||||
if (s.SkillId == 24u) runSkill = total;
|
if (s.SkillId == 24u) runSkill = total;
|
||||||
else if (s.SkillId == 22u) jumpSkill = total;
|
else if (s.SkillId == 22u) jumpSkill = total;
|
||||||
|
|
||||||
|
if (dumpPd)
|
||||||
|
Console.WriteLine(
|
||||||
|
$"vitals: PD-skill id={s.SkillId} init={s.Init} ranks={s.Ranks} formulaBonus={formulaBonus} total={total}");
|
||||||
}
|
}
|
||||||
if (runSkill >= 0 || jumpSkill >= 0)
|
if (runSkill >= 0 || jumpSkill >= 0)
|
||||||
{
|
|
||||||
if (dumpPd)
|
|
||||||
Console.WriteLine($"vitals: PD-skills run={runSkill} jump={jumpSkill}");
|
|
||||||
onSkillsUpdated(runSkill, jumpSkill);
|
onSkillsUpdated(runSkill, jumpSkill);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue #7 — enchantment block: feed each entry into the
|
// Issue #7 — enchantment block: feed each entry into the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue