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,
|
||||
PERSONAL_EXCERPT_MAX_LENGTH,
|
||||
} from "@/lib/profile/excerpt-limits";
|
||||
import { promoteMember as promoteMemberService } from "@/lib/promotions";
|
||||
|
||||
interface ActionResult<T = undefined> {
|
||||
success: boolean;
|
||||
|
|
@ -270,3 +271,22 @@ export async function updateIntelExcerpt(input: {
|
|||
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 { LeadershipEvaluationSection } from "@/components/frontend/profile/LeadershipEvaluationSection";
|
||||
import { DossierExcerptEditors } from "@/components/frontend/profile/DossierExcerptEditors";
|
||||
import { PromoteButton } from "@/components/frontend/profile/PromoteButton";
|
||||
import { getLeadershipDisplayLabel } from "@/lib/evaluations/levels";
|
||||
import { allowedPromotionTargets, resolvePromotionAuthority } from "@/lib/promotions";
|
||||
import type {
|
||||
EvaluationMissionOption,
|
||||
EvaluationSummary,
|
||||
|
|
@ -361,6 +363,10 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||
: null;
|
||||
const canSubmitEvaluation = viewer !== null && !isProfileOwner;
|
||||
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(
|
||||
(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 ──────────────────────────────────── */}
|
||||
<section className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ export const Profiles: CollectionConfig = {
|
|||
aimTrainer?: { xpEarned?: number | null };
|
||||
opordRecall?: { xpEarned?: number | null };
|
||||
poker?: { xpEarned?: number | null };
|
||||
blackjack?: { xpEarned?: number | null };
|
||||
};
|
||||
voiceover?: { xpEarned?: number | null };
|
||||
};
|
||||
|
|
@ -150,6 +151,7 @@ export const Profiles: CollectionConfig = {
|
|||
const aimTrainerXp = progressionData?.minigames?.aimTrainer?.xpEarned ?? 0;
|
||||
const opordRecallXp = progressionData?.minigames?.opordRecall?.xpEarned ?? 0;
|
||||
const pokerXp = progressionData?.minigames?.poker?.xpEarned ?? 0;
|
||||
const blackjackXp = progressionData?.minigames?.blackjack?.xpEarned ?? 0;
|
||||
const voiceoverXp = progressionData?.voiceover?.xpEarned ?? 0;
|
||||
|
||||
if (awardIds.length > 0) {
|
||||
|
|
@ -195,6 +197,8 @@ export const Profiles: CollectionConfig = {
|
|||
xpAcc += opordRecallXp;
|
||||
// Poker sit-and-go: XP from placement payouts
|
||||
xpAcc += pokerXp;
|
||||
// Blackjack sessions: XP from profit-based payouts
|
||||
xpAcc += blackjackXp;
|
||||
// Voiceover submissions: XP awarded per accepted submission
|
||||
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",
|
||||
label: "Voiceover",
|
||||
|
|
|
|||
Loading…
Reference in a new issue