diff --git a/packages/chess/src/net/prediction.ts b/packages/chess/src/net/prediction.ts index b91ee20..66bc95a 100644 --- a/packages/chess/src/net/prediction.ts +++ b/packages/chess/src/net/prediction.ts @@ -130,10 +130,23 @@ export class PredictionManager { * previously-legal optimistic moves, and re-validating them here would * duplicate server logic. Simpler to let the next user action * re-predict against the freshly-synced base. + * + * We route through `setActivePresets` (not bare `activePresets.replaceAll`) + * so preset `onActivate` / `onDeactivate` lifecycle hooks fire on the + * client's base engine. That's essential for rules like `piece-hp` + * which install per-piece state (Hp facts) from onActivate — without + * this the client would know the preset is active but have no Hp + * facts to render, because `game.presets` carries only the + * activation list, not the resulting facts. + * + * Idempotence guarantee: preset hooks are expected to be idempotent + * (piece-hp.onActivate only inserts Hp when absent). Server and + * client run the same hook logic, so both arrive at the same state. + * The next `game.state` or `game.delta` reconciles any drift. */ private applyPresets(payload: GamePresetsPayload): void { try { - this.baseEngine.activePresets.replaceAll(payload.activations); + this.baseEngine.setActivePresets(payload.activations); } catch { // A bad set from the server shouldn't crash the client; the server // already validated, so this branch is defensive only. We clear