diff --git a/src/components/frontend/minigames/opord/OpordArena.tsx b/src/components/frontend/minigames/opord/OpordArena.tsx index 4aca4b9..d153404 100644 --- a/src/components/frontend/minigames/opord/OpordArena.tsx +++ b/src/components/frontend/minigames/opord/OpordArena.tsx @@ -150,8 +150,9 @@ export function OpordArena({ Debrief A perfect run inside {OPORD_DIFFICULTIES[difficulty].parTimeMs / 1000}s earns a{" "} - {OPORD_DIFFICULTIES[difficulty].perfectBonus}-point bonus. Each tier tracks its own - best score. + {OPORD_DIFFICULTIES[difficulty].perfectBonus}-point bonus. Finishing under the par + time also earns a speed bonus that scales with how fast you finish and how many + answers you got right. Each tier tracks its own best score. diff --git a/src/lib/minigames/opord-recall.ts b/src/lib/minigames/opord-recall.ts index 6338cbe..60b84ca 100644 --- a/src/lib/minigames/opord-recall.ts +++ b/src/lib/minigames/opord-recall.ts @@ -26,6 +26,12 @@ export interface OpordTierConfig { readonly parTimeMs: number; /** Bonus for a perfect run inside the par time. */ readonly perfectBonus: number; + /** + * Points pool for the time bonus: a run finishing under the par time earns + * this scaled by the fraction of par time saved and the fraction of + * questions answered correctly, so speed pays only alongside accuracy. + */ + readonly maxSpeedBonus: number; } /** Grace added to the quiz limit for network/rounding before the server rejects. */ @@ -50,6 +56,7 @@ export const OPORD_DIFFICULTIES: Record = { correctPoints: 100, parTimeMs: 100_000, perfectBonus: 150, + maxSpeedBonus: 150, }, veteran: { studyHintSeconds: 25, @@ -58,6 +65,7 @@ export const OPORD_DIFFICULTIES: Record = { correctPoints: 150, parTimeMs: 150_000, perfectBonus: 300, + maxSpeedBonus: 250, }, elite: { // Extra clock headroom: elite relational questions include time arithmetic. @@ -67,6 +75,7 @@ export const OPORD_DIFFICULTIES: Record = { correctPoints: 200, parTimeMs: 200_000, perfectBonus: 500, + maxSpeedBonus: 400, }, }; @@ -103,7 +112,15 @@ export function calculateOpordScore( ): number { const tier = OPORD_DIFFICULTIES[difficulty]; const perfect = correct >= tier.questionCount && timeMs <= tier.parTimeMs; - return Math.max(0, correct * tier.correctPoints + (perfect ? tier.perfectBonus : 0)); + const speedBonus = + timeMs < tier.parTimeMs + ? Math.round( + ((tier.parTimeMs - timeMs) / tier.parTimeMs) * + tier.maxSpeedBonus * + (correct / tier.questionCount), + ) + : 0; + return Math.max(0, correct * tier.correctPoints + speedBonus + (perfect ? tier.perfectBonus : 0)); } export function calculateOpordXp(score: number, isNewTierBest: boolean): number {