171 lines
4.6 KiB
TypeScript
171 lines
4.6 KiB
TypeScript
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<Record<Iwt2BossEntityState['bossId'], BossMotionProfile>> = {
|
|
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')
|
|
)
|
|
}
|