Connect Leader Skill battle effects and private Rihani exchanges

Prototype Mentat/Bureaucrat score effects, automatic Sandmaster board spice, and Rihani inspection plus committed exchange. Preserve private views, physical custody, saved winner cleanup and captive/Suk sequencing. Remaining normal bands, missing skill families and combined modules remain gated.

Validation: 45 focused tests; typecheck, lint, 4465 offline tests, production build and 43 HTTP tests passed. Independent rules/privacy/persistence review completed. New isolated browser setup exercised inspection, draw, pending refresh, exchange and completed refresh. Two genuine four-profile Basic/Advanced samples completed without rejected candidates but did not trigger Rihani.

Preserved all 393 opening rooms and original seat/recovery rows (427 rooms after QA/HTTP). SQLite integrity and foreign keys passed. Server remained healthy after the reported outage; no restart or reset.

Verified source tree: c64ce9537269de94c0d785bc08b17346e826c5797352a636771e1dc6ce5e4859
This commit is contained in:
Quintine committed 2026-09-13 19:45:29 +10:00
1 parent af360c836b
commit 06f46c6f79
26 files changed
+1230 -122

No files matched your search

+28 -1
View File
@@ -24,7 +24,7 @@ export type BattleLeaderSkill = {
};
export type AppliedLeaderSkill = {
skill: DirectLeaderSkillId | 'planetologist';
skill: DirectLeaderSkillId | 'planetologist' | 'mentat';
amount: 1 | 2 | 3;
mode: 'normal' | 'skilled';
};
@@ -114,6 +114,30 @@ function isDirectSkill(skill: string): skill is DirectLeaderSkillId {
return directSkills.has(skill as LeaderSkillId);
}
/** A captive grants only this actual disc's lower effect, never a normal effect. */
export function usesSurvivingSkilledLeader(
assignments: readonly BattleLeaderSkill[],
skill: LeaderSkillId,
leader: string | undefined | null,
survives: boolean,
): boolean {
return survives && !!leader && assignments.some((a) =>
a.skill === skill && a.leader === leader && (!a.faceUp || a.captured));
}
/** Bureaucrat changes the opponent's total, not either leader disc or bounty. */
export function bureaucratBattlePenalty(
assignments: readonly BattleLeaderSkill[],
leader: string | undefined | null,
survives: boolean,
opponentStrongholds: number | undefined,
): number {
if (!usesSurvivingSkilledLeader(assignments, 'bureaucrat', leader, survives)) return 0;
if (opponentStrongholds === undefined || !Number.isSafeInteger(opponentStrongholds) || opponentStrongholds < 0)
throw new Error('Bureaucrat needs a valid occupied stronghold count.');
return opponentStrongholds;
}
function qualifies(
skill: DirectLeaderSkillId,
weapon: Card | undefined,
@@ -144,6 +168,9 @@ export function leaderSkillBattleBonus(input: {
}): LeaderSkillBattleBonus {
if (!input.selectedLeader) return { bonus: 0, applied: [] };
const applied: AppliedLeaderSkill[] = [];
if (input.selectedLeader.kind === 'disc' && usesSurvivingSkilledLeader(
input.assignments, 'mentat', input.selectedLeader.id, input.skilledLeaderSurvives,
)) applied.push({ skill: 'mentat', amount: 2, mode: 'skilled' });
if (
input.selectedLeader.kind === 'disc' &&
input.skilledLeaderSurvives &&