perf: adaptive performance mode for mobile

Auto-detect low-end devices (< 4 cores or touch) and reduce visual
complexity: 60% fewer particles, 6 vs 16 speed lines, 15 vs 40 stars,
halved datacenter buildings/racks, skip LED blinking animations,
fewer GPU graveyard chips, reduced afterimages and exhaust particles.

Toggle in fight viewer (LO/HI button), persisted in localStorage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 00:00:48 +00:00
co-authored by Claude Opus 4.6
parent 78f83a7520
commit 5af5ee1cba
4 changed files with 97 additions and 31 deletions
+40 -28
View File
@@ -8,7 +8,7 @@ import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MO
import {
CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO,
CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF,
TIER_ULTIMATE_CHANCES,
TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles,
} from './fight/config'
import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles'
import { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects'
@@ -361,9 +361,10 @@ export async function createFightScene(config: FightSceneConfig) {
// Hyperspeed lines — converging toward a point
function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) {
const lineCount = isPerfMode() ? 6 : 16
const lines: GameObj[] = []
for (let i = 0; i < 16; i++) {
const angle = (i / 16) * Math.PI * 2
for (let i = 0; i < lineCount; i++) {
const angle = (i / lineCount) * Math.PI * 2
const dist = 300 + Math.random() * 200
const sx = targetX + Math.cos(angle) * dist
const sy = targetY + Math.sin(angle) * dist
@@ -415,7 +416,8 @@ export async function createFightScene(config: FightSceneConfig) {
// Spawn afterimages: trailing ghost sprites behind a moving fighter
function spawnAfterimages(fighter: Fighter, count: number = 3) {
if (!fighter?.exists()) return
for (let i = 0; i < count; i++) {
const n = isPerfMode() ? Math.max(1, Math.round(count * 0.5)) : count
for (let i = 0; i < n; i++) {
const ghost = k.add([
k.rect(FRAME_SIZE * Math.abs(fighter.scale.x), FRAME_SIZE * Math.abs(fighter.scale.y)),
k.pos(fighter.pos.x - i * 8 * Math.sign(fighter.scale.x), fighter.pos.y),
@@ -456,7 +458,8 @@ export async function createFightScene(config: FightSceneConfig) {
const acc = theme.accent
// Parallax stars/particles in the sky
for (let i = 0; i < 40; i++) {
const starCount = isPerfMode() ? 15 : 40
for (let i = 0; i < starCount; i++) {
const star = k.add([
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)),
@@ -472,21 +475,25 @@ export async function createFightScene(config: FightSceneConfig) {
}
if (a === 'datacenter' || a === 'localhost') {
const perf = isPerfMode()
// City skyline silhouette behind the racks
const buildingHeights = [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42]
const buildingHeights = perf
? [0.55, 0.65, 0.7, 0.6, 0.72, 0.58]
: [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42]
for (let i = 0; i < buildingHeights.length; i++) {
const bw = W / buildingHeights.length + 4
const bh = GROUND_Y * buildingHeights[i]
const bx = i * (W / buildingHeights.length)
k.add([k.rect(bw, bh), k.pos(bx, GROUND_Y - bh), k.color(safeColor(k, '#060610')), k.opacity(0.5), k.z(0)])
const winStep = perf ? 14 : 7
for (let wy = 0; wy < bh - 10; wy += 8) {
for (let wx = 4; wx < bw - 6; wx += 7) {
for (let wx = 4; wx < bw - 6; wx += winStep) {
const win = k.add([
k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy),
k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')),
k.opacity(0.15 + Math.random() * 0.2), k.z(0),
])
if (Math.random() < 0.15) {
if (!perf && Math.random() < 0.15) {
const blink = 0.03 + Math.random() * 0.06
win.onUpdate(() => { win.opacity = Math.random() > blink ? win.opacity : (Math.random() > 0.5 ? 0.3 : 0.05) })
}
@@ -494,11 +501,13 @@ export async function createFightScene(config: FightSceneConfig) {
}
}
// Server rack wall
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 10; col++) {
const lx = 15 + col * (W / 10)
const rackRows = perf ? 2 : 4
const rackCols = perf ? 5 : 10
for (let row = 0; row < rackRows; row++) {
for (let col = 0; col < rackCols; col++) {
const lx = 15 + col * (W / rackCols)
const ly = 15 + row * 55
const rackW = W / 10 - 8
const rackW = W / rackCols - 8
const rackH = 48
k.add([k.rect(rackW, rackH), k.pos(lx, ly), k.color(safeColor(k, '#06060f')), k.opacity(0.6), k.z(1)])
k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
@@ -507,21 +516,23 @@ export async function createFightScene(config: FightSceneConfig) {
for (let s = 0; s < 5; s++) {
k.add([k.rect(rackW - 6, 1), k.pos(lx + 3, ly + 8 + s * 8), k.color(safeColor(k, '#111122')), k.opacity(0.5), k.z(1)])
}
for (let led = 0; led < 3; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
k.color(safeColor(k, Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')),
k.opacity(0.7), k.z(2),
])
const blinkRate = 0.02 + Math.random() * 0.08
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > blinkRate ? 0.7 : 0.15 })
}
if (Math.random() < 0.4) {
const actLed = k.add([
k.rect(2, 2), k.pos(lx + rackW - 8, ly + 4),
k.color(safeColor(k, '#ffaa00')), k.opacity(0.6), k.z(2),
])
actLed.onUpdate(() => { actLed.opacity = Math.random() > 0.5 ? 0.7 : 0.2 })
if (!perf) {
for (let led = 0; led < 3; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
k.color(safeColor(k, Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')),
k.opacity(0.7), k.z(2),
])
const blinkRate = 0.02 + Math.random() * 0.08
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > blinkRate ? 0.7 : 0.15 })
}
if (Math.random() < 0.4) {
const actLed = k.add([
k.rect(2, 2), k.pos(lx + rackW - 8, ly + 4),
k.color(safeColor(k, '#ffaa00')), k.opacity(0.6), k.z(2),
])
actLed.onUpdate(() => { actLed.opacity = Math.random() > 0.5 ? 0.7 : 0.2 })
}
}
}
}
@@ -536,7 +547,8 @@ export async function createFightScene(config: FightSceneConfig) {
} else if (a === 'gpu_graveyard') {
// Floating circuit board fragments
for (let i = 0; i < 16; i++) {
const chipCount = isPerfMode() ? 6 : 16
for (let i = 0; i < chipCount; i++) {
const cx = Math.random() * W
const cy = 20 + Math.random() * (GROUND_Y - 60)
const cw = 15 + Math.random() * 25