import type { Iwt2BossEntityState } from '../sim' export type BossRenderMotion = { x: number y: number scaleX: number scaleY: number rotation: number tint?: number } type BossMotionProfile = { idleFrequency: number idleBob: number moveFrequency: number moveBob: number impactScale: number } const DEFAULT_PROFILE: BossMotionProfile = { idleFrequency: 1.6, idleBob: 2.1, moveFrequency: 5.4, moveBob: 3, impactScale: 0.035, } const BOSS_PROFILES: Partial> = { bulldrome: { idleFrequency: 1.35, idleBob: 2.3, moveFrequency: 5.9, moveBob: 3.2, impactScale: 0.045, }, 'yian-kut-ku': { idleFrequency: 2.2, idleBob: 2.8, moveFrequency: 6.4, moveBob: 3.4, impactScale: 0.032, }, 'obsidian-ram-golem': { idleFrequency: 1.05, idleBob: 1.6, moveFrequency: 3.7, moveBob: 2.5, impactScale: 0.052, }, } const WARM_TINT = 0xffc88a const IMPACT_TINT = 0xff9b5f export function bossRenderMotion(entity: Iwt2BossEntityState, timeSeconds: number): BossRenderMotion { const profile = BOSS_PROFILES[entity.bossId] ?? DEFAULT_PROFILE const phase = String(entity.attackPhase) const speed = Math.hypot(entity.velocity.x, entity.velocity.y) const moving = speed > 8 || phase === 'relocating' const facingX = entity.facing.x < -0.05 ? -1 : 1 const motion: BossRenderMotion = { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, } if (isActionPhase(phase, 'windup')) return windupMotion(motion, entity, phase, timeSeconds, facingX) if (isBurstPhase(phase)) return burstMotion(motion, timeSeconds, facingX) if (isActionPhase(phase, 'recover')) return recoverMotion(motion, profile, entity, timeSeconds) if (moving) return movingMotion(motion, profile, entity, timeSeconds, facingX) return idleMotion(motion, profile, timeSeconds) } function idleMotion(motion: BossRenderMotion, profile: BossMotionProfile, timeSeconds: number): BossRenderMotion { const wave = Math.sin(timeSeconds * profile.idleFrequency * Math.PI * 2) return { ...motion, y: -Math.max(0, wave) * profile.idleBob, scaleY: 1 + Math.max(0, wave) * 0.025, scaleX: 1 - Math.max(0, wave) * 0.01, } } function movingMotion( motion: BossRenderMotion, profile: BossMotionProfile, entity: Iwt2BossEntityState, timeSeconds: number, facingX: number, ): BossRenderMotion { const speedFactor = Math.min(1, Math.hypot(entity.velocity.x, entity.velocity.y) / 180) const stride = Math.sin(timeSeconds * profile.moveFrequency * Math.PI * 2) const lift = Math.abs(stride) * profile.moveBob return { ...motion, y: -lift, scaleX: 1 + 0.012 * speedFactor, scaleY: 1 - 0.009 * Math.abs(stride), rotation: facingX * 0.025 * speedFactor, } } function windupMotion( motion: BossRenderMotion, entity: Iwt2BossEntityState, phase: string, timeSeconds: number, facingX: number, ): BossRenderMotion { const pulse = 0.5 + Math.sin(timeSeconds * Math.PI * 9) * 0.5 const isSlam = phase.includes('slam') || phase.includes('quake') || phase.includes('shatter') return { ...motion, x: -facingX * (isSlam ? 1.5 : 3), y: isSlam ? -2 : 2, scaleX: 1.035, scaleY: 0.965, rotation: -facingX * 0.02, tint: pulse > 0.45 || entity.phaseSecondsRemaining < 0.18 ? WARM_TINT : undefined, } } function burstMotion(motion: BossRenderMotion, timeSeconds: number, facingX: number): BossRenderMotion { const shake = Math.sin(timeSeconds * Math.PI * 38) * 1.8 return { ...motion, x: facingX * 3 + shake, y: Math.cos(timeSeconds * Math.PI * 42) * 1.2, scaleX: 1.045, scaleY: 0.96, rotation: facingX * 0.018, tint: WARM_TINT, } } function recoverMotion( motion: BossRenderMotion, profile: BossMotionProfile, entity: Iwt2BossEntityState, timeSeconds: number, ): BossRenderMotion { const settle = Math.max(0, Math.min(1, entity.phaseSecondsRemaining / 0.45)) const impactPulse = Math.abs(Math.sin(timeSeconds * Math.PI * 11)) * settle const pop = profile.impactScale * impactPulse return { ...motion, y: -2 * impactPulse, scaleX: 1 + pop, scaleY: 1 + pop * 0.45, tint: impactPulse > 0.45 ? IMPACT_TINT : undefined, } } function isActionPhase(phase: string, suffix: string): boolean { return phase.toLowerCase().includes(suffix) } function isBurstPhase(phase: string): boolean { const normalized = phase.toLowerCase() return ( normalized.includes('charging') || normalized.includes('swooping') || normalized.includes('burrowing') || normalized.includes('ricocheting') || normalized.includes('flying') ) }