feat: enrich game cards with bestFps, raw/poor performer badges, and battery estimates (Tasks 5.1-5.6)
This commit is contained in:
@@ -40,6 +40,10 @@ interface GamesListItem {
|
||||
deckStatus: string | null
|
||||
antiCheatRelevant: boolean
|
||||
antiCheatStatus: "none" | "supported" | "unsupported" | "unknown" | null
|
||||
bestFps: number | null
|
||||
isRawPerformer: boolean
|
||||
isPoorPerformance: boolean
|
||||
estimatedBatteryMin: number | null
|
||||
}
|
||||
|
||||
interface DeviceOption {
|
||||
@@ -914,6 +918,26 @@ function GameCard({ game }: { game: GamesListItem }) {
|
||||
{game.steamReviewScore}%
|
||||
</span>
|
||||
)}
|
||||
{game.isRawPerformer && (
|
||||
<span className='inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold shrink-0'>
|
||||
⚡ RAW PERFORMER
|
||||
</span>
|
||||
)}
|
||||
{game.isPoorPerformance && (
|
||||
<span className='inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-500/10 border border-red-500/20 text-red-400 text-[10px] font-semibold shrink-0'>
|
||||
⚠ POOR PERFORMANCE
|
||||
</span>
|
||||
)}
|
||||
{game.bestFps != null && (
|
||||
<span className='inline-flex items-center gap-1 text-[10px] text-text/50'>
|
||||
{Math.round(game.bestFps)}fps best
|
||||
</span>
|
||||
)}
|
||||
{game.estimatedBatteryMin != null && (
|
||||
<span className='inline-flex items-center gap-1 text-[10px] text-text/50'>
|
||||
🔋 ~{Math.round(game.estimatedBatteryMin / 60)}h
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-1.5 flex items-center gap-2 flex-wrap'>
|
||||
{game.benchmarkCount > 0 ? (
|
||||
|
||||
@@ -123,6 +123,71 @@ export default async function GamesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Performance stats: best FPS, raw performer, poor performance, battery estimate
|
||||
const rawPerformerMap = new Map<string, boolean>()
|
||||
const poorPerformerMap = new Map<string, boolean>()
|
||||
const bestFpsMap = new Map<string, number>()
|
||||
const batteryMinMap = new Map<string, number>()
|
||||
|
||||
if (gameIds.length > 0) {
|
||||
const perfStats = await db
|
||||
.select({
|
||||
gameId: gameVersions.gameId,
|
||||
bestFps: sql<number>`MAX(${performanceEntries.fpsAvg})::real`,
|
||||
isRawPerformer: sql<boolean>`BOOL_OR(
|
||||
${performanceEntries.fpsAvg} >= 60
|
||||
AND ${performanceEntries.upscalerType} = 'none'
|
||||
AND ${performanceEntries.frameGenMethod} = 'none'
|
||||
)`,
|
||||
isPoorPerformance: sql<boolean>`BOOL_OR(${performanceEntries.fpsAvg} < 30)`,
|
||||
})
|
||||
.from(performanceEntries)
|
||||
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
|
||||
.where(
|
||||
and(
|
||||
inArray(gameVersions.gameId, gameIds),
|
||||
eq(performanceEntries.isRemoved, false),
|
||||
),
|
||||
)
|
||||
.groupBy(gameVersions.gameId)
|
||||
|
||||
for (const row of perfStats) {
|
||||
bestFpsMap.set(row.gameId, row.bestFps)
|
||||
rawPerformerMap.set(row.gameId, row.isRawPerformer)
|
||||
poorPerformerMap.set(row.gameId, row.isPoorPerformance)
|
||||
}
|
||||
|
||||
// Battery estimate for handheld devices
|
||||
const batteryStats = await db
|
||||
.select({
|
||||
gameId: gameVersions.gameId,
|
||||
estimatedBatteryMin: sql<number>`ROUND(
|
||||
(${hardware.wattHours}::real / ${performanceEntries.tdpWatts}) * 60
|
||||
)::int`,
|
||||
})
|
||||
.from(performanceEntries)
|
||||
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
|
||||
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
|
||||
.where(
|
||||
and(
|
||||
inArray(gameVersions.gameId, gameIds),
|
||||
eq(performanceEntries.isRemoved, false),
|
||||
eq(hardware.deviceType, "handheld"),
|
||||
sql`${performanceEntries.tdpWatts} IS NOT NULL AND ${performanceEntries.tdpWatts} > 0`,
|
||||
sql`${hardware.wattHours} IS NOT NULL`,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(performanceEntries.fpsAvg))
|
||||
|
||||
const seenGames = new Set<string>()
|
||||
for (const row of batteryStats) {
|
||||
if (!seenGames.has(row.gameId)) {
|
||||
seenGames.add(row.gameId)
|
||||
batteryMinMap.set(row.gameId, row.estimatedBatteryMin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all genres
|
||||
const genreRows = await db
|
||||
.select({ genres: games.genres })
|
||||
@@ -161,6 +226,10 @@ export default async function GamesPage() {
|
||||
deckStatus: platformMap.get(g.id) ?? null,
|
||||
antiCheatRelevant: antiCheatMap.get(g.id)?.antiCheatRelevant ?? false,
|
||||
antiCheatStatus: antiCheatMap.get(g.id)?.antiCheatStatus ?? null,
|
||||
bestFps: bestFpsMap.get(g.id) ?? null,
|
||||
isRawPerformer: rawPerformerMap.get(g.id) ?? false,
|
||||
isPoorPerformance: poorPerformerMap.get(g.id) ?? false,
|
||||
estimatedBatteryMin: batteryMinMap.get(g.id) ?? null,
|
||||
}))
|
||||
|
||||
const allGenres = Array.from(genreSet).sort()
|
||||
|
||||
@@ -49,6 +49,7 @@ interface UnifiedResult {
|
||||
isRawPerformer?: boolean
|
||||
isPoorPerformance?: boolean
|
||||
bestFps?: number | null
|
||||
estimatedBatteryMin?: number | null
|
||||
latestVersion?: string | null
|
||||
tinyImage?: string | null
|
||||
// Badges & review fields
|
||||
@@ -567,6 +568,11 @@ function SearchResultCard({
|
||||
⚠ POOR PERFORMANCE
|
||||
</span>
|
||||
)}
|
||||
{result.estimatedBatteryMin != null && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-[10px] font-semibold shrink-0">
|
||||
🔋 ~{Math.round(result.estimatedBatteryMin / 60)}h
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(result.developer || result.publisher) && (
|
||||
<p className="text-[11px] text-text/45 mt-0.5 truncate">
|
||||
@@ -751,6 +757,14 @@ function SearchResultCard({
|
||||
color={result.bestFps != null && result.bestFps >= 60 ? "text-green-400" : undefined}
|
||||
/>
|
||||
|
||||
{/* Battery Estimate */}
|
||||
{result.estimatedBatteryMin != null && (
|
||||
<DataField
|
||||
label="Battery"
|
||||
value={`~${Math.round(result.estimatedBatteryMin / 60)}h`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Version */}
|
||||
<DataField
|
||||
label="Version"
|
||||
|
||||
Reference in New Issue
Block a user