fix: allow Google Fonts in CSP so 3D announcement font loads in production
fontSrc blocked fonts.gstatic.com and styleSrc blocked fonts.googleapis.com, causing Honk font to fail in production. The chromatic aberration pseudo-elements then appeared as doubled plain text instead of a styled 3D effect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b28d0b220e
commit
7b82803868
@@ -7,7 +7,15 @@ export type { FightSceneConfig, RoundEvent } from './fight/types'
|
||||
import { ARENA_THEMES, spriteAnims } from './fight/constants'
|
||||
import { pickChoreography } from './fight/choreography/picker'
|
||||
export { pickChoreography }
|
||||
import { isPerfMode, perfParticles, isReducedMotion, haptic } from './fight/config'
|
||||
import {
|
||||
isPerfMode, perfParticles, isReducedMotion, haptic,
|
||||
GROUND_Y_RATIO, HOME_A_RATIO, HOME_B_RATIO, REFERENCE_HEIGHT, SPRITE_LOAD_TIMEOUT_MS,
|
||||
FIGHTER_BASE_SCALE, FIGHTER_TIER_SCALE, JUDGE_SCALE, HUMAN_COACH_SCALE, HUMAN_RUN_SCALE,
|
||||
AMBIENT_PARTICLE_COUNT, SKY_GRADIENT_BANDS, GROUND_DEPTH_LINES, PERSPECTIVE_GRID_LINES, PILLAR_WIDTH,
|
||||
SPEECH_BUBBLE_MAX_CHARS, SPEECH_BUBBLE_WRAP, SPEECH_BUBBLE_MAX_LINES, SPEECH_BUBBLE_FONT_SIZE, SPEECH_BUBBLE_DEFAULT_DURATION,
|
||||
HUMAN_SHOW_CHANCE, HUMAN_COACH_CHANCE, HUMAN_RUN_CHANCE,
|
||||
HAPTIC_MIN_SHAKE, HAPTIC_SCALE,
|
||||
} 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'
|
||||
import { spawnProjectile as _spawnProjectile } from './fight/projectiles'
|
||||
@@ -63,7 +71,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
// Wrap k.shake to support reduced motion + haptic feedback
|
||||
const _origShake = k.shake.bind(k)
|
||||
k.shake = ((intensity: number) => {
|
||||
if (intensity >= 3) haptic(Math.min(100, intensity * 8))
|
||||
if (intensity >= HAPTIC_MIN_SHAKE) haptic(Math.min(100, intensity * HAPTIC_SCALE))
|
||||
if (!isReducedMotion()) _origShake(intensity)
|
||||
}) as typeof k.shake
|
||||
|
||||
@@ -102,7 +110,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
try {
|
||||
await Promise.race([
|
||||
k.loadSprite(name, src, opts),
|
||||
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('timeout')), 5_000)),
|
||||
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('timeout')), SPRITE_LOAD_TIMEOUT_MS)),
|
||||
])
|
||||
} catch (err) {
|
||||
console.warn(`[FightScene] Sprite '${name}' failed/timed out:`, err)
|
||||
@@ -185,10 +193,10 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
const vhsTracking = (duration: number = 0.4) => _vhsTracking(fightCtx, duration)
|
||||
const dimensionalShift = (duration: number = 0.6) => _dimensionalShift(fightCtx, duration)
|
||||
|
||||
const SF = Math.min(1, H / 500)
|
||||
const GROUND_Y = H * 0.78
|
||||
const HOME_A = W * 0.28
|
||||
const HOME_B = W * 0.72
|
||||
const SF = Math.min(1, H / REFERENCE_HEIGHT)
|
||||
const GROUND_Y = H * GROUND_Y_RATIO
|
||||
const HOME_A = W * HOME_A_RATIO
|
||||
const HOME_B = W * HOME_B_RATIO
|
||||
|
||||
|
||||
|
||||
@@ -356,8 +364,8 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
// === SKY / BACKGROUND ATMOSPHERE ===
|
||||
// Vertical gradient backdrop — 6 bands from dark ceiling to ground horizon
|
||||
const bgBase = theme.bg
|
||||
for (let band = 0; band < 6; band++) {
|
||||
const bandH = GROUND_Y / 6
|
||||
for (let band = 0; band < SKY_GRADIENT_BANDS; band++) {
|
||||
const bandH = GROUND_Y / SKY_GRADIENT_BANDS
|
||||
const brighten = band * 3
|
||||
k.add([
|
||||
k.rect(W, bandH + 1), k.pos(0, band * bandH),
|
||||
@@ -379,14 +387,14 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
// Secondary highlight
|
||||
k.add([k.rect(W, 1), k.pos(0, GROUND_Y + 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3)])
|
||||
// Ground gradient — fades deeper
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
for (let i = 1; i <= GROUND_DEPTH_LINES; i++) {
|
||||
k.add([k.rect(W, 2), k.pos(0, GROUND_Y + 3 + i * 6), k.color(safeColor(k,theme.accent)), k.opacity(0.1 - i * 0.007)])
|
||||
}
|
||||
// Perspective grid on the floor — converging lines for 3D depth
|
||||
const vanishX = W / 2
|
||||
const vanishY = GROUND_Y * 0.6
|
||||
for (let i = 0; i <= 24; i++) {
|
||||
const x = i * (W / 24)
|
||||
for (let i = 0; i <= PERSPECTIVE_GRID_LINES; i++) {
|
||||
const x = i * (W / PERSPECTIVE_GRID_LINES)
|
||||
// Lines from floor edge pointing toward vanishing point
|
||||
const dx = x - vanishX
|
||||
const topX = vanishX + dx * 0.3
|
||||
@@ -405,15 +413,15 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
|
||||
// === SIDE PILLARS / FRAME ===
|
||||
// Subtle dark pillars on edges for framing
|
||||
k.add([k.rect(8, GROUND_Y), k.pos(0, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
|
||||
k.add([k.rect(8, GROUND_Y), k.pos(W - 8, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
|
||||
k.add([k.rect(PILLAR_WIDTH, GROUND_Y), k.pos(0, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
|
||||
k.add([k.rect(PILLAR_WIDTH, GROUND_Y), k.pos(W - PILLAR_WIDTH, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
|
||||
// Accent trim on pillars
|
||||
k.add([k.rect(1, GROUND_Y), k.pos(8, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
|
||||
k.add([k.rect(1, GROUND_Y), k.pos(W - 9, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
|
||||
k.add([k.rect(1, GROUND_Y), k.pos(PILLAR_WIDTH, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
|
||||
k.add([k.rect(1, GROUND_Y), k.pos(W - PILLAR_WIDTH - 1, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
|
||||
|
||||
// === AMBIENT PARTICLES ===
|
||||
// Floating dust/embers in the air (subtle, always present)
|
||||
for (let i = 0; i < 18; i++) {
|
||||
for (let i = 0; i < AMBIENT_PARTICLE_COUNT; i++) {
|
||||
const particle = k.add([
|
||||
k.circle(0.5 + Math.random() * 1.5),
|
||||
k.pos(Math.random() * W, Math.random() * GROUND_Y),
|
||||
@@ -454,16 +462,16 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
k.sprite('judge', { anim: 'idle' }),
|
||||
k.pos(CHAIR_X, CHAIR_SEAT_Y),
|
||||
k.anchor('bot'),
|
||||
k.scale(1.3 * SF),
|
||||
k.scale(JUDGE_SCALE * SF),
|
||||
k.z(4),
|
||||
k.opacity(0.9),
|
||||
'judge',
|
||||
])
|
||||
|
||||
const scaleA = (1.8 + botA.tier * 0.4) * SF
|
||||
const scaleA = (FIGHTER_BASE_SCALE + botA.tier * FIGHTER_TIER_SCALE) * SF
|
||||
k.add([k.sprite('botA', { anim: 'idle' }), k.pos(-100, GROUND_Y - 6), k.anchor('bot'), k.scale(scaleA), k.z(10), k.opacity(0), 'fighterA'])
|
||||
|
||||
const scaleB = (1.8 + botB.tier * 0.4) * SF
|
||||
const scaleB = (FIGHTER_BASE_SCALE + botB.tier * FIGHTER_TIER_SCALE) * SF
|
||||
k.add([k.sprite('botB', { anim: 'idle' }), k.pos(W + 100, GROUND_Y - 6), k.anchor('bot'), k.scale(-scaleB, scaleB), k.z(10), k.opacity(0), 'fighterB'])
|
||||
|
||||
config.onReady?.()
|
||||
@@ -599,7 +607,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
els.length = 0
|
||||
}
|
||||
|
||||
function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = 3) {
|
||||
function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = SPEECH_BUBBLE_DEFAULT_DURATION) {
|
||||
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
|
||||
if (!fighter || !text) return
|
||||
|
||||
@@ -612,10 +620,9 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
// Sanitize and truncate
|
||||
const clean = safeText(text)
|
||||
if (!clean) return
|
||||
const maxLen = 80
|
||||
const display = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '...' : clean
|
||||
const lines = wrapBubbleText(display, 18)
|
||||
const fontSize = 12
|
||||
const display = clean.length > SPEECH_BUBBLE_MAX_CHARS ? clean.slice(0, SPEECH_BUBBLE_MAX_CHARS - 1) + '...' : clean
|
||||
const lines = wrapBubbleText(display, SPEECH_BUBBLE_WRAP)
|
||||
const fontSize = SPEECH_BUBBLE_FONT_SIZE
|
||||
const lineH = fontSize + 4
|
||||
const padX = 10, padY = 8
|
||||
const textW = Math.max(60, Math.min(lines.reduce((m, l) => Math.max(m, l.length * (fontSize * 0.6)), 0) + 8, W * 0.38))
|
||||
@@ -740,9 +747,8 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
}
|
||||
}
|
||||
if (line) lines.push(line)
|
||||
// Max 5 lines, last line truncated
|
||||
if (lines.length > 5) {
|
||||
const truncated = [...lines.slice(0, 4), lines.slice(4).join(' ').slice(0, maxChars - 3) + '...']
|
||||
if (lines.length > SPEECH_BUBBLE_MAX_LINES) {
|
||||
const truncated = [...lines.slice(0, SPEECH_BUBBLE_MAX_LINES - 1), lines.slice(SPEECH_BUBBLE_MAX_LINES - 1).join(' ').slice(0, maxChars - 3) + '...']
|
||||
return truncated
|
||||
}
|
||||
return lines
|
||||
@@ -892,7 +898,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
k.sprite(spriteId, { anim: mood }),
|
||||
k.pos(homeX, GROUND_Y - 4),
|
||||
k.anchor('bot'),
|
||||
k.scale(side === 'a' ? 1.2 : -1.2, 1.2),
|
||||
k.scale(side === 'a' ? HUMAN_COACH_SCALE : -HUMAN_COACH_SCALE, HUMAN_COACH_SCALE),
|
||||
k.z(5),
|
||||
k.opacity(0),
|
||||
tag,
|
||||
@@ -917,7 +923,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
k.sprite(spriteId, { anim: 'run' }),
|
||||
k.pos(startX, GROUND_Y - 4),
|
||||
k.anchor('bot'),
|
||||
k.scale(fromLeft ? 1.3 : -1.3, 1.3),
|
||||
k.scale(fromLeft ? HUMAN_RUN_SCALE : -HUMAN_RUN_SCALE, HUMAN_RUN_SCALE),
|
||||
k.z(8),
|
||||
k.opacity(0.9),
|
||||
])
|
||||
@@ -971,16 +977,16 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
/** Random chance to show a human moment — called between rounds */
|
||||
async function maybeShowHuman() {
|
||||
const roll = Math.random()
|
||||
if (roll > 0.15) return // 15% chance per round
|
||||
if (roll > HUMAN_SHOW_CHANCE) return
|
||||
|
||||
const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b'
|
||||
const action = Math.random()
|
||||
|
||||
if (action < 0.4) {
|
||||
if (action < HUMAN_COACH_CHANCE) {
|
||||
// Coach appearance (most common)
|
||||
const mood = Math.random() < 0.5 ? 'cheer' : 'panic'
|
||||
await spawnHumanCoach(side, mood)
|
||||
} else if (action < 0.7) {
|
||||
} else if (action < HUMAN_RUN_CHANCE) {
|
||||
// Run across screen
|
||||
await humanRunAcross(side)
|
||||
} else {
|
||||
|
||||
@@ -80,9 +80,43 @@ export const PHYSICAL_CONTACT_BASE = 0.35 // Base chance for brawl/cli
|
||||
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 MAYBE_SHOW_HUMAN_CHANCE = 0.2 // Show human coach
|
||||
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
|
||||
|
||||
// --- Scene layout ---
|
||||
export const GROUND_Y_RATIO = 0.78 // Ground line as fraction of canvas height
|
||||
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
|
||||
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
|
||||
export const SPEECH_BUBBLE_MAX_CHARS = 80 // Max chars in speech bubble
|
||||
export const SPEECH_BUBBLE_WRAP = 18 // Chars per line in speech bubble
|
||||
export const SPEECH_BUBBLE_MAX_LINES = 5 // Max lines in speech bubble
|
||||
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)
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
CREATOR_CAMEO_CHANCE, DODGE_CHANCE, COUNTER_ATTACK_CHANCE,
|
||||
SCANLINE_GLITCH_CHANCE, COMEDY_SFX_CHANCE,
|
||||
PHYSICAL_CONTACT_BASE, PHYSICAL_CONTACT_INTENSITY_SCALE, PHYSICAL_CONTACT_EXCHANGE_BONUS,
|
||||
CROWD_REACTION_CHANCE, MAYBE_SHOW_HUMAN_CHANCE, CREATOR_VOICE_LINE_CHANCE,
|
||||
CROWD_REACTION_CHANCE, CREATOR_VOICE_LINE_CHANCE,
|
||||
HYPERDETAIL_BASE_CHANCE, HYPERDETAIL_INTENSITY_SCALE,
|
||||
} from './config'
|
||||
|
||||
|
||||
+2
-2
@@ -41,10 +41,10 @@ app.use('*', secureHeaders({
|
||||
contentSecurityPolicy: process.env.NODE_ENV === 'production' ? {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
connectSrc: ["'self'"],
|
||||
fontSrc: ["'self'"],
|
||||
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
|
||||
} : undefined,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { ELO_DIVISOR, TIER_THRESHOLDS } from '../lib/constants.js'
|
||||
import {
|
||||
ELO_DIVISOR, TIER_THRESHOLDS,
|
||||
TIMEOUT_DAMAGE_MULTIPLIER, CRITICAL_MARGIN_THRESHOLD, CRITICAL_DAMAGE_MULTIPLIER,
|
||||
MARGIN_TO_DAMAGE_SCALE, LOSER_DAMAGE_BASE, ARENA_DAMAGE_MULTIPLIER,
|
||||
MAX_COMBO_STACKS, COMBO_DAMAGE_PER_STACK,
|
||||
FACTUAL_FASTER_BASE, FACTUAL_SLOWER_BASE, SPEED_ADVANTAGE_MULTIPLIER, SPEED_RATIO_MULTIPLIER,
|
||||
ONE_CORRECT_WINNER_BASE, CONFIDENCE_BONUS, NO_ANSWER_SCORE, BOTH_WRONG_FASTER, BOTH_WRONG_SLOWER,
|
||||
EMPTY_RESPONSE_QUALITY, MIN_QUALITY_LENGTH, SPAM_CHAR_RATIO,
|
||||
QUALITY_LENGTH_IDEAL_MIN, QUALITY_LENGTH_IDEAL_MAX, QUALITY_LENGTH_SECONDARY_MAX,
|
||||
QUALITY_SCORE_IDEAL, QUALITY_SCORE_SECONDARY, QUALITY_SCORE_LONG,
|
||||
WORD_DIVERSITY_SCALE, WORD_DIVERSITY_CAP, SPEED_BONUS_BASE,
|
||||
RETRO_SPEED_BONUS_CAP,
|
||||
LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD,
|
||||
DEFAULT_CHALLENGE_TIMEOUT_MS,
|
||||
} from '../lib/constants.js'
|
||||
import type { Challenge } from './challenges.js'
|
||||
import { pick } from '../lib/utils.js'
|
||||
import { checkAnswer } from './answers.js'
|
||||
|
||||
@@ -31,3 +31,46 @@ export const DEFAULT_CHALLENGE_TIMEOUT_MS = 8000
|
||||
// --- Fight loop ---
|
||||
export const FIGHT_LOOP_INTERVAL_MS = 8000
|
||||
export const ELO_MATCHING_RANDOMNESS = 150
|
||||
|
||||
// --- Scoring: damage ---
|
||||
export const TIMEOUT_DAMAGE_MULTIPLIER = 1.5 // Damage multiplier for timeout/error losses
|
||||
export const CRITICAL_MARGIN_THRESHOLD = 4 // Score margin above which a hit is critical
|
||||
export const CRITICAL_DAMAGE_MULTIPLIER = 1.5 // Damage multiplied on critical hits
|
||||
export const MARGIN_TO_DAMAGE_SCALE = 2 // baseDamage + margin * this
|
||||
export const LOSER_DAMAGE_BASE = 0.3 // Loser gets baseDamage * this - margin
|
||||
export const ARENA_DAMAGE_MULTIPLIER = 2 // Damage multiplier when arena modifier matches
|
||||
|
||||
// --- Scoring: combo ---
|
||||
export const MAX_COMBO_STACKS = 5 // Combo cap before scaling stops
|
||||
export const COMBO_DAMAGE_PER_STACK = 0.2 // Damage increase per combo stack
|
||||
|
||||
// --- Scoring: factual ---
|
||||
export const FACTUAL_FASTER_BASE = 7 // Base score: correct + faster
|
||||
export const FACTUAL_SLOWER_BASE = 5 // Base score: correct + slower
|
||||
export const SPEED_ADVANTAGE_MULTIPLIER = 2 // Speed advantage scaling
|
||||
export const SPEED_RATIO_MULTIPLIER = 1.5 // Speed ratio scaling for slower bot
|
||||
export const ONE_CORRECT_WINNER_BASE = 9 // Base score: only answerer correct
|
||||
export const CONFIDENCE_BONUS = 0.5 // Multiplier on confidence for correct answer
|
||||
export const NO_ANSWER_SCORE = 1 // Score when opponent has no/wrong answer
|
||||
export const BOTH_WRONG_FASTER = 4 // Both wrong, faster bot
|
||||
export const BOTH_WRONG_SLOWER = 3 // Both wrong, slower bot
|
||||
|
||||
// --- Scoring: creative quality ---
|
||||
export const EMPTY_RESPONSE_QUALITY = 0.5 // Quality score for null/empty answers
|
||||
export const MIN_QUALITY_LENGTH = 10 // Below this: minimum quality
|
||||
export const SPAM_CHAR_RATIO = 0.1 // Unique chars / length — below = spam
|
||||
export const QUALITY_LENGTH_IDEAL_MIN = 30 // Ideal response length minimum
|
||||
export const QUALITY_LENGTH_IDEAL_MAX = 400 // Ideal response length maximum
|
||||
export const QUALITY_LENGTH_SECONDARY_MAX = 600 // Secondary length cutoff
|
||||
export const QUALITY_SCORE_IDEAL = 4 // Length score: ideal range
|
||||
export const QUALITY_SCORE_SECONDARY = 3 // Length score: 400-600
|
||||
export const QUALITY_SCORE_LONG = 2 // Length score: 600+ or short
|
||||
export const WORD_DIVERSITY_SCALE = 4 // Diversity scaling factor
|
||||
export const WORD_DIVERSITY_CAP = 3 // Max diversity bonus
|
||||
export const SPEED_BONUS_BASE = 2 // Max speed bonus
|
||||
export const RETRO_SPEED_BONUS_CAP = 0.2 // Retro mode speed bonus cap
|
||||
|
||||
// --- Scoring: narration thresholds ---
|
||||
export const LARGE_WIN_MARGIN = 5 // Margin for "big win" narration
|
||||
export const CLOSE_MATCH_MARGIN = 3 // Margin for "close match" narration
|
||||
export const WHIFF_NARRATION_THRESHOLD = 2 // Whiff count for narration trigger
|
||||
|
||||
Reference in New Issue
Block a user