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:
co-authored by
Claude Opus 4.6
parent
78f83a7520
commit
5af5ee1cba
@@ -13,6 +13,7 @@ import {
|
|||||||
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
|
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
|
||||||
} from '../game/audio'
|
} from '../game/audio'
|
||||||
import { isKokoroLoading, getKokoroProgress } from '../game/tts'
|
import { isKokoroLoading, getKokoroProgress } from '../game/tts'
|
||||||
|
import { isPerfMode, setPerfMode } from '../game/fight/config'
|
||||||
|
|
||||||
interface Round {
|
interface Round {
|
||||||
roundNumber: number
|
roundNumber: number
|
||||||
@@ -73,6 +74,7 @@ const hitTextY = ref(30)
|
|||||||
const glitching = ref(false)
|
const glitching = ref(false)
|
||||||
const soundOn = ref(true)
|
const soundOn = ref(true)
|
||||||
const insanityMode = ref(false)
|
const insanityMode = ref(false)
|
||||||
|
const perfMode = ref(false)
|
||||||
|
|
||||||
// Zap the winner
|
// Zap the winner
|
||||||
const zapSent = ref(false)
|
const zapSent = ref(false)
|
||||||
@@ -155,6 +157,12 @@ function toggleInsanity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
perfMode.value = isPerfMode()
|
||||||
|
function togglePerfMode() {
|
||||||
|
perfMode.value = !perfMode.value
|
||||||
|
setPerfMode(perfMode.value)
|
||||||
|
}
|
||||||
|
|
||||||
// Staggered log
|
// Staggered log
|
||||||
const logItems = ref<{ type: string; round: number; text: string; color: string }[]>([])
|
const logItems = ref<{ type: string; round: number; text: string; color: string }[]>([])
|
||||||
|
|
||||||
@@ -808,6 +816,16 @@ async function _doReplay() {
|
|||||||
<path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>
|
<path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 flex items-center justify-center border text-[9px] font-pixel tracking-wider transition-all"
|
||||||
|
:class="perfMode
|
||||||
|
? 'border-neon-green/70 text-neon-green bg-neon-green/10'
|
||||||
|
: 'border-border/50 text-text-muted hover:text-neon-green hover:border-neon-green/50'"
|
||||||
|
:title="perfMode ? 'Performance mode ON (fewer particles)' : 'Performance mode OFF'"
|
||||||
|
@click="togglePerfMode"
|
||||||
|
>
|
||||||
|
{{ perfMode ? 'LO' : 'HI' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="isReplaying"
|
v-if="isReplaying"
|
||||||
class="h-8 px-2 flex items-center justify-center border text-[9px] font-pixel tracking-wider transition-all"
|
class="h-8 px-2 flex items-center justify-center border text-[9px] font-pixel tracking-wider transition-all"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MO
|
|||||||
import {
|
import {
|
||||||
CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO,
|
CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO,
|
||||||
CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF,
|
CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF,
|
||||||
TIER_ULTIMATE_CHANCES,
|
TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles,
|
||||||
} from './fight/config'
|
} from './fight/config'
|
||||||
import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles'
|
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'
|
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
|
// Hyperspeed lines — converging toward a point
|
||||||
function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) {
|
function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) {
|
||||||
|
const lineCount = isPerfMode() ? 6 : 16
|
||||||
const lines: GameObj[] = []
|
const lines: GameObj[] = []
|
||||||
for (let i = 0; i < 16; i++) {
|
for (let i = 0; i < lineCount; i++) {
|
||||||
const angle = (i / 16) * Math.PI * 2
|
const angle = (i / lineCount) * Math.PI * 2
|
||||||
const dist = 300 + Math.random() * 200
|
const dist = 300 + Math.random() * 200
|
||||||
const sx = targetX + Math.cos(angle) * dist
|
const sx = targetX + Math.cos(angle) * dist
|
||||||
const sy = targetY + Math.sin(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
|
// Spawn afterimages: trailing ghost sprites behind a moving fighter
|
||||||
function spawnAfterimages(fighter: Fighter, count: number = 3) {
|
function spawnAfterimages(fighter: Fighter, count: number = 3) {
|
||||||
if (!fighter?.exists()) return
|
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([
|
const ghost = k.add([
|
||||||
k.rect(FRAME_SIZE * Math.abs(fighter.scale.x), FRAME_SIZE * Math.abs(fighter.scale.y)),
|
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),
|
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
|
const acc = theme.accent
|
||||||
|
|
||||||
// Parallax stars/particles in the sky
|
// 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([
|
const star = k.add([
|
||||||
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
|
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
|
||||||
k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)),
|
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') {
|
if (a === 'datacenter' || a === 'localhost') {
|
||||||
|
const perf = isPerfMode()
|
||||||
// City skyline silhouette behind the racks
|
// 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++) {
|
for (let i = 0; i < buildingHeights.length; i++) {
|
||||||
const bw = W / buildingHeights.length + 4
|
const bw = W / buildingHeights.length + 4
|
||||||
const bh = GROUND_Y * buildingHeights[i]
|
const bh = GROUND_Y * buildingHeights[i]
|
||||||
const bx = i * (W / buildingHeights.length)
|
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)])
|
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 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([
|
const win = k.add([
|
||||||
k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy),
|
k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy),
|
||||||
k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')),
|
k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')),
|
||||||
k.opacity(0.15 + Math.random() * 0.2), k.z(0),
|
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
|
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) })
|
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
|
// Server rack wall
|
||||||
for (let row = 0; row < 4; row++) {
|
const rackRows = perf ? 2 : 4
|
||||||
for (let col = 0; col < 10; col++) {
|
const rackCols = perf ? 5 : 10
|
||||||
const lx = 15 + col * (W / 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 ly = 15 + row * 55
|
||||||
const rackW = W / 10 - 8
|
const rackW = W / rackCols - 8
|
||||||
const rackH = 48
|
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, 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)])
|
k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
|
||||||
@@ -507,6 +516,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
for (let s = 0; s < 5; s++) {
|
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)])
|
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)])
|
||||||
}
|
}
|
||||||
|
if (!perf) {
|
||||||
for (let led = 0; led < 3; led++) {
|
for (let led = 0; led < 3; led++) {
|
||||||
const ledEl = k.add([
|
const ledEl = k.add([
|
||||||
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
|
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
|
||||||
@@ -525,6 +535,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Cable bundles
|
// Cable bundles
|
||||||
for (let c = 0; c < 6; c++) {
|
for (let c = 0; c < 6; c++) {
|
||||||
const cy = 3 + c * 2
|
const cy = 3 + c * 2
|
||||||
@@ -536,7 +547,8 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
|
|
||||||
} else if (a === 'gpu_graveyard') {
|
} else if (a === 'gpu_graveyard') {
|
||||||
// Floating circuit board fragments
|
// 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 cx = Math.random() * W
|
||||||
const cy = 20 + Math.random() * (GROUND_Y - 60)
|
const cy = 20 + Math.random() * (GROUND_Y - 60)
|
||||||
const cw = 15 + Math.random() * 25
|
const cw = 15 + Math.random() * 25
|
||||||
|
|||||||
@@ -2,6 +2,38 @@
|
|||||||
// Gameplay constants — all tunable magic numbers in one place
|
// Gameplay constants — all tunable magic numbers in one place
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// --- Performance mode ---
|
||||||
|
const PERF_KEY = 'bf_perf_mode'
|
||||||
|
|
||||||
|
function detectLowEnd(): boolean {
|
||||||
|
if (typeof navigator === 'undefined') return false
|
||||||
|
const cores = navigator.hardwareConcurrency || 4
|
||||||
|
const mobile = navigator.maxTouchPoints > 0
|
||||||
|
return cores < 4 || mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPerfPref(): boolean | null {
|
||||||
|
try {
|
||||||
|
const v = localStorage.getItem(PERF_KEY)
|
||||||
|
if (v === '1') return true
|
||||||
|
if (v === '0') return false
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let _perfMode = loadPerfPref() ?? detectLowEnd()
|
||||||
|
|
||||||
|
export function isPerfMode(): boolean { return _perfMode }
|
||||||
|
export function setPerfMode(on: boolean): void {
|
||||||
|
_perfMode = on
|
||||||
|
try { localStorage.setItem(PERF_KEY, on ? '1' : '0') } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scale a particle count down in performance mode (40%) */
|
||||||
|
export function perfParticles(count: number): number {
|
||||||
|
return _perfMode ? Math.max(1, Math.round(count * 0.4)) : count
|
||||||
|
}
|
||||||
|
|
||||||
// --- Choreography selection probabilities ---
|
// --- Choreography selection probabilities ---
|
||||||
export const CREATOR_ULTIMATE_CHANCE = 0.5 // Creator ultimate chance (non-crit); 1.0 on crits
|
export const CREATOR_ULTIMATE_CHANCE = 0.5 // Creator ultimate chance (non-crit); 1.0 on crits
|
||||||
export const CREATOR_THEMED_MOVE_RATIO = 0.6 // Creator uses creator-themed moves 60% of the time
|
export const CREATOR_THEMED_MOVE_RATIO = 0.6 // Creator uses creator-themed moves 60% of the time
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { FightContext } from './types'
|
import type { FightContext } from './types'
|
||||||
|
import { perfParticles, isPerfMode } from './config'
|
||||||
|
|
||||||
export function spawnSparks(ctx: FightContext, x: number, y: number, count: number, color: string): void {
|
export function spawnSparks(ctx: FightContext, x: number, y: number, count: number, color: string): void {
|
||||||
const { k, safeColor } = ctx
|
const { k, safeColor } = ctx
|
||||||
|
const n = perfParticles(count)
|
||||||
const intense = count > 8
|
const intense = count > 8
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const angle = Math.random() * Math.PI * 2
|
const angle = Math.random() * Math.PI * 2
|
||||||
const speed = (intense ? 150 : 100) + Math.random() * (intense ? 400 : 300)
|
const speed = (intense ? 150 : 100) + Math.random() * (intense ? 400 : 300)
|
||||||
const size = (intense ? 3 : 2) + Math.random() * (intense ? 6 : 4)
|
const size = (intense ? 3 : 2) + Math.random() * (intense ? 6 : 4)
|
||||||
@@ -31,7 +33,8 @@ export function spawnSparks(ctx: FightContext, x: number, y: number, count: numb
|
|||||||
|
|
||||||
export function spawnBulletHoles(ctx: FightContext, x: number, y: number, count: number): void {
|
export function spawnBulletHoles(ctx: FightContext, x: number, y: number, count: number): void {
|
||||||
const { k, safeColor, trackedTimeout, trackedInterval, clearTracked } = ctx
|
const { k, safeColor, trackedTimeout, trackedInterval, clearTracked } = ctx
|
||||||
for (let i = 0; i < count; i++) {
|
const n = perfParticles(count)
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
const hole = k.add([
|
const hole = k.add([
|
||||||
k.circle(3 + Math.random() * 3),
|
k.circle(3 + Math.random() * 3),
|
||||||
k.pos(x + (Math.random() - 0.5) * 40, y + (Math.random() - 0.5) * 60),
|
k.pos(x + (Math.random() - 0.5) * 40, y + (Math.random() - 0.5) * 60),
|
||||||
@@ -53,7 +56,8 @@ export function spawnExhaust(ctx: FightContext, x: number, y: number, duration:
|
|||||||
const endTime = performance.now() + duration * 1000
|
const endTime = performance.now() + duration * 1000
|
||||||
const interval = trackedInterval(() => {
|
const interval = trackedInterval(() => {
|
||||||
if (performance.now() > endTime) { clearTracked(interval); return }
|
if (performance.now() > endTime) { clearTracked(interval); return }
|
||||||
for (let i = 0; i < 3; i++) {
|
const exhaustCount = isPerfMode() ? 1 : 3
|
||||||
|
for (let i = 0; i < exhaustCount; i++) {
|
||||||
const p = k.add([
|
const p = k.add([
|
||||||
k.circle(3 + Math.random() * 5),
|
k.circle(3 + Math.random() * 5),
|
||||||
k.pos(x + (Math.random() - 0.5) * 15, y),
|
k.pos(x + (Math.random() - 0.5) * 15, y),
|
||||||
|
|||||||
Reference in New Issue
Block a user