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 {
|
||||
|
||||
Reference in New Issue
Block a user