All files / web/src/lib/arcade song-context.ts

97.93% Statements 95/97
74.07% Branches 20/27
100% Functions 7/7
97.93% Lines 95/97

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 981x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 15x 15x 1x 1x 135x 135x 135x 135x 135x 125x     1x 1x 25x 25x 25x 25x 25x 25x 25x 25x 81x 81x 71x 71x 71x 81x 25x 25x 25x 1x 1x 24x 24x 24x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 9x 9x 1x 1x 20x 20x 20x 20x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x  
import type { GameSongContext } from './game-sdk/types'
 
export type SongMomentCategory =
  | 'Opening beat'
  | 'Clutch moment'
  | 'Near miss'
  | 'Clean run'
  | 'Perfect run'
  | 'Comeback'
  | 'Tough item'
  | 'Strategy'
  | 'Signature items'
 
const DEFAULT_PHRASE_LIMIT = 120
 
export type GameSongContextDraft = Omit<
  GameSongContext,
  'details' | 'dramaticMoments' | 'strategyNotes'
> & {
  details?: Array<string | null | undefined>
  dramaticMoments?: Array<string | null | undefined>
  strategyNotes?: Array<string | null | undefined>
}
 
export function pluralize(count: number, singular: string, plural = `${singular}s`): string {
  return count === 1 ? singular : plural
}
 
export function compactSongPhrase(
  value: string | null | undefined,
  maxLength = DEFAULT_PHRASE_LIMIT
) {
  const compacted = value?.replace(/\s+/g, ' ').trim()
  if (!compacted) return undefined
  if (compacted.length <= maxLength) return compacted
  return `${compacted.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`
}
 
export function uniqueSongPhrases(
  values: Array<string | null | undefined>,
  limit: number,
  maxLength = DEFAULT_PHRASE_LIMIT
): string[] {
  const seen = new Set<string>()
  const result: string[] = []
 
  for (const value of values) {
    const compacted = compactSongPhrase(value, maxLength)
    if (!compacted || seen.has(compacted)) continue
    seen.add(compacted)
    result.push(compacted)
    if (result.length >= limit) break
  }
 
  return result
}
 
export function songDetail(label: string, value: string | number | null | undefined) {
  if (value == null) return undefined
  return compactSongPhrase(`${label}: ${value}`)
}
 
export function songList(
  label: string,
  values: Array<string | number | null | undefined>,
  limit: number
) {
  const items = uniqueSongPhrases(
    values.map((value) => (value == null ? undefined : String(value))),
    limit,
    48
  )
  if (items.length === 0) return undefined
  return songDetail(label, items.join(', '))
}
 
export function songMoment(category: SongMomentCategory, detail: string | null | undefined) {
  const compacted = compactSongPhrase(detail)
  if (!compacted) return undefined
  return `${category}: ${compacted}`
}
 
export function finalizeSongContext(context: GameSongContextDraft): GameSongContext {
  const summary = compactSongPhrase(context.summary, 140)
  const details = uniqueSongPhrases(context.details ?? [], 6)
  const dramaticMoments = uniqueSongPhrases(context.dramaticMoments ?? [], 7)
  const strategyNotes = uniqueSongPhrases(context.strategyNotes ?? [], 4)
  const outcome = compactSongPhrase(context.outcome, 100)
 
  return {
    ...(summary && { summary }),
    ...(details.length > 0 && { details }),
    ...(dramaticMoments.length > 0 && { dramaticMoments }),
    ...(strategyNotes.length > 0 && { strategyNotes }),
    ...(outcome && { outcome }),
  }
}