2026-03-08 23:34:05 +00:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Gameplay constants — all tunable magic numbers in one place
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
2026-03-09 00:00:48 +00:00
|
|
|
|
// --- 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 {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 00:23:39 +00:00
|
|
|
|
/** Scale a particle count down in performance mode (40%) or reduced motion (20%) */
|
2026-03-09 00:00:48 +00:00
|
|
|
|
export function perfParticles(count: number): number {
|
2026-03-09 00:23:39 +00:00
|
|
|
|
if (_reducedMotion) return Math.max(1, Math.round(count * 0.2))
|
2026-03-09 00:00:48 +00:00
|
|
|
|
return _perfMode ? Math.max(1, Math.round(count * 0.4)) : count
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 00:23:39 +00:00
|
|
|
|
// --- Reduced motion ---
|
|
|
|
|
|
const _reducedMotion = typeof window !== 'undefined'
|
|
|
|
|
|
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
|
|
|
|
: false
|
|
|
|
|
|
|
|
|
|
|
|
export function isReducedMotion(): boolean { return _reducedMotion }
|
|
|
|
|
|
|
|
|
|
|
|
// --- Haptic feedback ---
|
|
|
|
|
|
export function haptic(ms: number = 50): void {
|
|
|
|
|
|
try { navigator?.vibrate?.(ms) } catch { /* no vibration support */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 23:34:05 +00:00
|
|
|
|
// --- Choreography selection probabilities ---
|
|
|
|
|
|
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 CRIT_THEMED_RATIO = 0.5 // On crits: 50% themed, 50% epic generic
|
|
|
|
|
|
export const CHOREOGRAPHY_THEMED_RATIO = 0.6 // Normal: 60% themed
|
|
|
|
|
|
export const CHOREOGRAPHY_GENERIC_CUTOFF = 0.85 // Normal: 25% generic (0.60–0.85), 15% wild (0.85–1.0)
|
|
|
|
|
|
|
|
|
|
|
|
// --- Tier-gated ultimate chances ---
|
|
|
|
|
|
export const TIER_ULTIMATE_CHANCES: Record<number, number> = {
|
|
|
|
|
|
2: 0.15,
|
|
|
|
|
|
3: 0.20,
|
|
|
|
|
|
4: 0.30,
|
|
|
|
|
|
5: 0.40,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Morph system ---
|
|
|
|
|
|
export const CREATOR_MORPH_CRIT_CHANCE = 0.7 // Creator morphs on devastating crits 70%
|
|
|
|
|
|
export const CREATOR_MORPH_INTENSITY_THRESHOLD = 0.6 // Creator morphs when intensity > 0.6
|
|
|
|
|
|
export const BOT_MORPH_CRIT_CHANCE = 0.4 // Regular bots morph on devastating crits 40%
|
|
|
|
|
|
export const BOT_MORPH_INTENSITY_THRESHOLD = 0.7 // Regular bots morph when intensity > 0.7
|
|
|
|
|
|
|
|
|
|
|
|
// --- Creator cameo ---
|
|
|
|
|
|
export const CREATOR_CAMEO_CHANCE = 0.06 // 6% per round in non-creator fights
|
|
|
|
|
|
|
|
|
|
|
|
// --- Round exchange probabilities ---
|
|
|
|
|
|
export const DODGE_CHANCE = 0.12 // Defender dodges instead of being hit
|
|
|
|
|
|
export const COUNTER_ATTACK_CHANCE = 0.25 // Defender counters after getting hit
|
|
|
|
|
|
export const SCANLINE_GLITCH_CHANCE = 0.25 // Random scanline glitch during exchanges
|
|
|
|
|
|
export const COMEDY_SFX_CHANCE = 0.15 // Comedy sound on non-decisive hits
|
|
|
|
|
|
export const PHYSICAL_CONTACT_BASE = 0.35 // Base chance for brawl/clinch
|
|
|
|
|
|
export const PHYSICAL_CONTACT_INTENSITY_SCALE = 0.2 // Added per unit of intensity
|
|
|
|
|
|
export const PHYSICAL_CONTACT_EXCHANGE_BONUS = 0.15 // Added after first exchange
|
|
|
|
|
|
export const CROWD_REACTION_CHANCE = 0.4 // Crowd reacts to round results
|
|
|
|
|
|
export const CREATOR_VOICE_LINE_CHANCE = 0.6 // Creator gets a voice line
|
|
|
|
|
|
|
|
|
|
|
|
// --- Hyperdetail (grotesque close-up) ---
|
|
|
|
|
|
export const HYPERDETAIL_BASE_CHANCE = 0.05 // Base hyperdetail chance
|
|
|
|
|
|
export const HYPERDETAIL_INTENSITY_SCALE = 0.45 // Scales with intensity
|
2026-03-09 07:18:57 +00:00
|
|
|
|
|
|
|
|
|
|
// --- Scene layout ---
|
2026-03-09 08:21:16 +00:00
|
|
|
|
export const GROUND_Y_RATIO = 0.82 // Ground line as fraction of canvas height
|
2026-03-09 07:18:57 +00:00
|
|
|
|
export const HOME_A_RATIO = 0.28 // Fighter A home X position ratio
|
|
|
|
|
|
export const HOME_B_RATIO = 0.72 // Fighter B home X position ratio
|
|
|
|
|
|
export const REFERENCE_HEIGHT = 500 // Reference canvas height for scale factor
|
2026-03-09 08:21:16 +00:00
|
|
|
|
export const MOBILE_SCALE_CAP = 0.55 // Cap SF on small screens so fighters aren't too tiny
|
2026-03-09 07:18:57 +00:00
|
|
|
|
export const SPRITE_LOAD_TIMEOUT_MS = 5_000 // Max wait for sprite loading (mobile hang prevention)
|
|
|
|
|
|
|
|
|
|
|
|
// --- Fighter scaling ---
|
|
|
|
|
|
export const FIGHTER_BASE_SCALE = 1.8 // Base fighter sprite scale
|
|
|
|
|
|
export const FIGHTER_TIER_SCALE = 0.4 // Additional scale per tier level
|
|
|
|
|
|
export const JUDGE_SCALE = 1.3 // Judge sprite scale
|
|
|
|
|
|
export const HUMAN_COACH_SCALE = 1.2 // Human coach sprite scale
|
|
|
|
|
|
export const HUMAN_RUN_SCALE = 1.3 // Human run-across sprite scale
|
|
|
|
|
|
|
|
|
|
|
|
// --- Scene visual constants ---
|
|
|
|
|
|
export const AMBIENT_PARTICLE_COUNT = 18 // Floating dust/embers in air
|
|
|
|
|
|
export const SKY_GRADIENT_BANDS = 6 // Number of sky gradient bands
|
|
|
|
|
|
export const GROUND_DEPTH_LINES = 12 // Ground gradient fade lines
|
|
|
|
|
|
export const PERSPECTIVE_GRID_LINES = 24 // Floor grid convergence lines
|
|
|
|
|
|
export const PILLAR_WIDTH = 8 // Side pillar frame width
|
2026-03-09 14:11:02 +00:00
|
|
|
|
export const SPEECH_BUBBLE_MAX_CHARS = 200 // Max chars in speech bubble
|
|
|
|
|
|
export const SPEECH_BUBBLE_WRAP = 24 // Chars per line in speech bubble
|
|
|
|
|
|
export const SPEECH_BUBBLE_MAX_LINES = 8 // Max lines in speech bubble
|
2026-03-09 07:18:57 +00:00
|
|
|
|
export const SPEECH_BUBBLE_FONT_SIZE = 12 // Speech bubble font size
|
|
|
|
|
|
export const SPEECH_BUBBLE_DEFAULT_DURATION = 3 // Default speech bubble duration (seconds)
|
|
|
|
|
|
|
|
|
|
|
|
// --- Human appearance ---
|
|
|
|
|
|
export const HUMAN_SHOW_CHANCE = 0.15 // Chance per round for human appearance
|
|
|
|
|
|
export const HUMAN_COACH_CHANCE = 0.4 // Of appearances: coach (most common)
|
|
|
|
|
|
export const HUMAN_RUN_CHANCE = 0.7 // Of appearances: run across (cumulative cutoff)
|
|
|
|
|
|
|
|
|
|
|
|
// --- Haptic scaling ---
|
|
|
|
|
|
export const HAPTIC_MIN_SHAKE = 3 // Min shake intensity to trigger haptic
|
|
|
|
|
|
export const HAPTIC_SCALE = 8 // Haptic ms = intensity * scale (capped 100)
|