feat(ui) Campaign CA CA4 #431: server-authoritative raises — the optimistic layer is deleted
Retail sends a raise and WAITS: one request in flight, the raise controls ghost, and displayed state changes only when the authoritative quality-change record lands (gmStatManagementUI @ 0x004F03F0 family, pinned in docs/research/2026-07-10-retail-panel-behavior-pseudocode.md §5, whose own conclusion names ApplyLocalRaise as the thing to remove). The optimistic layer predates the inbound parsers — it existed so the panel showed anything at all — and with CA2 delivering server truth it became strictly harmful: against ACE, a wrong TrainSkill cost fails SILENTLY, so the optimistic promote-and-debit could show a trained skill the server refused with nothing to ever correct it. Deleted: CharacterSheetProvider.ApplyLocalRaise + both spend helpers, and LocalPlayerState's six optimistic mutators (ApplyAttributeRaise, ApplyVitalRaise, ApplySkillRaise, ApplySkillTraining, DebitIntProperty, DebitInt64Property) with their tests. Added: the one-in-flight latch in HandleRaiseRequest, CharacterSheet.AwaitingRaise ghosting all raise controls, and gate release on every authoritative quality signal (attribute/character/player-property events unconditionally; vital events only release-and-refresh while a raise is in flight, so regen ticks stay out of the sheet-rebuild path). Panel unmount resets the gate — retail's awaiting flag lives on the panel instance. AP-73 NARROWS rather than retires: retail's release on a rejection that produces NO quality change is statically unverifiable, and ACE sends chat-only (Raise*) or nothing (RaiseSkill/TrainSkill) on failure; until the CA5 live check, a silently-rejected request leaves the controls ghosted until panel reopen — recorded with its observable symptom. Also verified for CA4: the train button sends the DAT-exact TrainedCost (ACE's silent exact-match rule), and there is correctly NO panel specialize send — retail/ACE specialize only via the SkillAlterationDevice item-use + confirmation round-trip, whose client seams (SendConfirmationResponse 0x0275, the 0x028B WeenieErrorWithString chat routing) already exist. Provider tests now pin the retail contract: send-without-mutation, one-in-flight, release-on-record, release-on- unmount, and the regen-tick rebuild guard. Full hermetic suite 15,327 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5781895977
commit
08b77e20a9
8 changed files with 152 additions and 339 deletions
|
|
@ -1112,8 +1112,10 @@ public static class CharacterStatController
|
|||
var sheet = data();
|
||||
long cost1 = GetRaiseCost(sheet, selectedIndex, amount: 1);
|
||||
long cost10 = GetRaiseCost(sheet, selectedIndex, amount: 10);
|
||||
bool affordable1 = cost1 > 0 && sheet.UnassignedXp >= cost1;
|
||||
bool affordable10 = cost10 > 0 && sheet.UnassignedXp >= cost10;
|
||||
// CA4 (retired AP-73): while a raise awaits its authoritative
|
||||
// record, retail ghosts the raise controls (one request in flight).
|
||||
bool affordable1 = !sheet.AwaitingRaise && cost1 > 0 && sheet.UnassignedXp >= cost1;
|
||||
bool affordable10 = !sheet.AwaitingRaise && cost10 > 0 && sheet.UnassignedXp >= cost10;
|
||||
|
||||
foreach (var b in allRaise1)
|
||||
{
|
||||
|
|
@ -1146,9 +1148,9 @@ public static class CharacterStatController
|
|||
|
||||
bool trained = selectedSkill.AdvancementClass >= CharacterSkillAdvancementClass.Trained;
|
||||
long cost = trained ? selectedSkill.RaiseCost : selectedSkill.TrainedCost;
|
||||
bool affordable = trained
|
||||
bool affordable = !sheet.AwaitingRaise && (trained
|
||||
? cost > 0 && sheet.UnassignedXp >= cost
|
||||
: cost > 0 && sheet.SkillCredits >= cost;
|
||||
: cost > 0 && sheet.SkillCredits >= cost);
|
||||
foreach (var b in allRaise1)
|
||||
{
|
||||
b.Visible = true;
|
||||
|
|
@ -1163,7 +1165,7 @@ public static class CharacterStatController
|
|||
if (trained)
|
||||
{
|
||||
long cost10 = selectedSkill.Raise10Cost;
|
||||
bool affordable10 = cost10 > 0 && sheet.UnassignedXp >= cost10;
|
||||
bool affordable10 = !sheet.AwaitingRaise && cost10 > 0 && sheet.UnassignedXp >= cost10;
|
||||
b.TrySetRetailState(affordable10
|
||||
? UiButtonStateMachine.Normal
|
||||
: UiButtonStateMachine.Ghosted);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue