feat(profile): promote action and blackjack stats in profile XP
Server action pairing the GM promote button with the promotion service, plus the blackjack minigame stats block counted into profile XP totals. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
77034a8e67
commit
17f3cc6f47
3 changed files with 94 additions and 0 deletions
|
|
@ -20,6 +20,7 @@ import {
|
||||||
INTEL_EXCERPT_MAX_LENGTH,
|
INTEL_EXCERPT_MAX_LENGTH,
|
||||||
PERSONAL_EXCERPT_MAX_LENGTH,
|
PERSONAL_EXCERPT_MAX_LENGTH,
|
||||||
} from "@/lib/profile/excerpt-limits";
|
} from "@/lib/profile/excerpt-limits";
|
||||||
|
import { promoteMember as promoteMemberService } from "@/lib/promotions";
|
||||||
|
|
||||||
interface ActionResult<T = undefined> {
|
interface ActionResult<T = undefined> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
@ -270,3 +271,22 @@ export async function updateIntelExcerpt(input: {
|
||||||
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
|
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promote a member to a new rank. Authority is resolved server-side from the
|
||||||
|
* actor's rank promotionCeiling (or admin/developer bypass); the client only
|
||||||
|
* supplies the target username and rank id.
|
||||||
|
*/
|
||||||
|
export async function promoteMember(
|
||||||
|
username: string,
|
||||||
|
rankId: number,
|
||||||
|
): Promise<ActionResult<{ profileId: number }>> {
|
||||||
|
try {
|
||||||
|
const { payload, user } = await authenticate();
|
||||||
|
const result = await promoteMemberService(payload, user, username, rankId);
|
||||||
|
return { success: true, data: { profileId: result.profileId } };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message === "Unauthorized") throw e;
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ import { ProfileGrantAwardButton } from "@/components/frontend/account/ProfileGr
|
||||||
import { RibbonRack } from "@/components/frontend/account/RibbonRack";
|
import { RibbonRack } from "@/components/frontend/account/RibbonRack";
|
||||||
import { LeadershipEvaluationSection } from "@/components/frontend/profile/LeadershipEvaluationSection";
|
import { LeadershipEvaluationSection } from "@/components/frontend/profile/LeadershipEvaluationSection";
|
||||||
import { DossierExcerptEditors } from "@/components/frontend/profile/DossierExcerptEditors";
|
import { DossierExcerptEditors } from "@/components/frontend/profile/DossierExcerptEditors";
|
||||||
|
import { PromoteButton } from "@/components/frontend/profile/PromoteButton";
|
||||||
import { getLeadershipDisplayLabel } from "@/lib/evaluations/levels";
|
import { getLeadershipDisplayLabel } from "@/lib/evaluations/levels";
|
||||||
|
import { allowedPromotionTargets, resolvePromotionAuthority } from "@/lib/promotions";
|
||||||
import type {
|
import type {
|
||||||
EvaluationMissionOption,
|
EvaluationMissionOption,
|
||||||
EvaluationSummary,
|
EvaluationSummary,
|
||||||
|
|
@ -361,6 +363,10 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||||
: null;
|
: null;
|
||||||
const canSubmitEvaluation = viewer !== null && !isProfileOwner;
|
const canSubmitEvaluation = viewer !== null && !isProfileOwner;
|
||||||
const rank = profile.rank as Rank;
|
const rank = profile.rank as Rank;
|
||||||
|
const promotionResult = viewer ? await resolvePromotionAuthority(payload, viewer) : null;
|
||||||
|
const allowedTargetRanks = promotionResult
|
||||||
|
? allowedPromotionTargets(promotionResult, rank)
|
||||||
|
: [];
|
||||||
const awardEntries = (profile.progression?.awards ?? []).filter(
|
const awardEntries = (profile.progression?.awards ?? []).filter(
|
||||||
(entry): entry is ProfileAwardEntry => typeof entry === "object" && entry !== null,
|
(entry): entry is ProfileAwardEntry => typeof entry === "object" && entry !== null,
|
||||||
);
|
);
|
||||||
|
|
@ -607,6 +613,14 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allowedTargetRanks.length > 0 && (
|
||||||
|
<PromoteButton
|
||||||
|
username={username}
|
||||||
|
currentRankName={rank.name}
|
||||||
|
targetRanks={allowedTargetRanks.map((r) => ({ id: r.id, name: r.name }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Combat Record ──────────────────────────────────── */}
|
{/* ── Combat Record ──────────────────────────────────── */}
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ export const Profiles: CollectionConfig = {
|
||||||
aimTrainer?: { xpEarned?: number | null };
|
aimTrainer?: { xpEarned?: number | null };
|
||||||
opordRecall?: { xpEarned?: number | null };
|
opordRecall?: { xpEarned?: number | null };
|
||||||
poker?: { xpEarned?: number | null };
|
poker?: { xpEarned?: number | null };
|
||||||
|
blackjack?: { xpEarned?: number | null };
|
||||||
};
|
};
|
||||||
voiceover?: { xpEarned?: number | null };
|
voiceover?: { xpEarned?: number | null };
|
||||||
};
|
};
|
||||||
|
|
@ -150,6 +151,7 @@ export const Profiles: CollectionConfig = {
|
||||||
const aimTrainerXp = progressionData?.minigames?.aimTrainer?.xpEarned ?? 0;
|
const aimTrainerXp = progressionData?.minigames?.aimTrainer?.xpEarned ?? 0;
|
||||||
const opordRecallXp = progressionData?.minigames?.opordRecall?.xpEarned ?? 0;
|
const opordRecallXp = progressionData?.minigames?.opordRecall?.xpEarned ?? 0;
|
||||||
const pokerXp = progressionData?.minigames?.poker?.xpEarned ?? 0;
|
const pokerXp = progressionData?.minigames?.poker?.xpEarned ?? 0;
|
||||||
|
const blackjackXp = progressionData?.minigames?.blackjack?.xpEarned ?? 0;
|
||||||
const voiceoverXp = progressionData?.voiceover?.xpEarned ?? 0;
|
const voiceoverXp = progressionData?.voiceover?.xpEarned ?? 0;
|
||||||
|
|
||||||
if (awardIds.length > 0) {
|
if (awardIds.length > 0) {
|
||||||
|
|
@ -195,6 +197,8 @@ export const Profiles: CollectionConfig = {
|
||||||
xpAcc += opordRecallXp;
|
xpAcc += opordRecallXp;
|
||||||
// Poker sit-and-go: XP from placement payouts
|
// Poker sit-and-go: XP from placement payouts
|
||||||
xpAcc += pokerXp;
|
xpAcc += pokerXp;
|
||||||
|
// Blackjack sessions: XP from profit-based payouts
|
||||||
|
xpAcc += blackjackXp;
|
||||||
// Voiceover submissions: XP awarded per accepted submission
|
// Voiceover submissions: XP awarded per accepted submission
|
||||||
xpAcc += voiceoverXp;
|
xpAcc += voiceoverXp;
|
||||||
|
|
||||||
|
|
@ -705,8 +709,64 @@ export const Profiles: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "blackjack",
|
||||||
|
label: "Blackjack",
|
||||||
|
type: "group",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "gamesPlayed",
|
||||||
|
label: "Sessions Played",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "roundsWon",
|
||||||
|
label: "Rounds Won",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blackjacksHit",
|
||||||
|
label: "Blackjacks Hit",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bestScore",
|
||||||
|
label: "Best Bankroll",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "xpEarned",
|
||||||
|
label: "XP Earned",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "chipBank",
|
||||||
|
label: "Chip Bank",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Persistent play-chip pool for the card minigames (poker, blackjack). Earned from the other minigames; never creates chips itself.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "chipBankRefilledAt",
|
||||||
|
label: "Chip Bank Top-Up At",
|
||||||
|
type: "date",
|
||||||
|
admin: {
|
||||||
|
description: "Last daily chip top-up claim; one claim per 24 hours.",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "voiceover",
|
name: "voiceover",
|
||||||
label: "Voiceover",
|
label: "Voiceover",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue