import type { Iwt2PartyEntityState } from '../sim' import type { Iwt2ClassWeaponLayer } from '../content/classes' export type PartyAttackPulse = { startedAt: number } export type PartyRenderMotion = { x: number y: number rotation: number scaleX: number scaleY: number tint?: number } export type PartyWeaponLayerMotion = { offsetXScale: number offsetYScale: number rotation: number } const ATTACK_DURATION_SECONDS = 0.22 const RANGED_ATTACK_DURATION_SECONDS = 0.16 export function partyRenderMotion( entity: Iwt2PartyEntityState, timeSeconds: number, attackPulse?: PartyAttackPulse, motion: PartyRenderMotion = createPartyRenderMotion(), ): PartyRenderMotion { const speed = Math.hypot(entity.velocity.x, entity.velocity.y) const facingX = entity.facing.x < -0.05 ? -1 : 1 resetPartyRenderMotion(motion) if (entity.health <= 0) { motion.rotation = facingX * 0.08 motion.scaleY = 0.92 return motion } if (entity.status.stunnedSeconds > 0 || entity.status.knockedDownSeconds > 0) { motion.y = 3 motion.rotation = -facingX * 0.12 motion.scaleY = 0.9 return motion } const attackProgress = attackPulse ? attackProgressFor(entity, timeSeconds, attackPulse) : 1 if (attackProgress < 1) { const strike = Math.sin(attackProgress * Math.PI) const ranged = entity.projectileSpeed > 0 motion.x = facingX * (ranged ? -3 : 4) * strike motion.y = -1.5 * strike motion.rotation = facingX * (ranged ? -0.05 : 0.09) * strike motion.scaleX = 1 + (ranged ? 0.012 : 0.035) * strike motion.scaleY = 1 - (ranged ? 0.006 : 0.018) * strike motion.tint = ranged ? 0xdff3ff : 0xffe1a6 return motion } if (entity.castSecondsRemaining > 0) { const pulse = 0.5 + Math.sin(timeSeconds * Math.PI * 12) * 0.5 motion.y = -1 - pulse * 1.5 motion.scaleX = 1 + pulse * 0.012 motion.scaleY = 1 + pulse * 0.012 motion.tint = entity.classId === 'mage' ? 0xefc4ff : 0xb9e3ff return motion } if (speed > 10) { const stride = Math.sin(timeSeconds * Math.PI * 9) const lift = Math.abs(stride) * Math.min(2.8, speed / 90) motion.y = -lift motion.rotation = facingX * 0.025 * Math.min(1, speed / 190) motion.scaleY = 1 - Math.abs(stride) * 0.01 return motion } const idle = Math.sin(timeSeconds * Math.PI * 2.2) motion.y = -Math.max(0, idle) * 0.9 motion.scaleY = 1 + Math.max(0, idle) * 0.008 return motion } export function isPartyAttackPulseActive( entity: Iwt2PartyEntityState, timeSeconds: number, attackPulse: PartyAttackPulse, ): boolean { return attackProgressFor(entity, timeSeconds, attackPulse) < 1 } export function partyWeaponLayerMotion( entity: Iwt2PartyEntityState, layer: Iwt2ClassWeaponLayer, timeSeconds: number, attackPulse?: PartyAttackPulse, motion: PartyWeaponLayerMotion = createPartyWeaponLayerMotion(), ): PartyWeaponLayerMotion { resetPartyWeaponLayerMotion(motion) if (!attackPulse || entity.health <= 0) return motion const progress = attackProgressFor(entity, timeSeconds, attackPulse) if (progress >= 1) return motion const strike = Math.sin(progress * Math.PI) const attackMotion = layer.attackMotion motion.offsetXScale = (attackMotion?.offsetXScale ?? 0) * strike motion.offsetYScale = (attackMotion?.offsetYScale ?? 0) * strike motion.rotation = (attackMotion?.rotation ?? 0) * strike return motion } function createPartyRenderMotion(): PartyRenderMotion { return { x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1 } } function resetPartyRenderMotion(motion: PartyRenderMotion) { motion.x = 0 motion.y = 0 motion.rotation = 0 motion.scaleX = 1 motion.scaleY = 1 motion.tint = undefined } function createPartyWeaponLayerMotion(): PartyWeaponLayerMotion { return { offsetXScale: 0, offsetYScale: 0, rotation: 0 } } function resetPartyWeaponLayerMotion(motion: PartyWeaponLayerMotion) { motion.offsetXScale = 0 motion.offsetYScale = 0 motion.rotation = 0 } function attackProgressFor( entity: Iwt2PartyEntityState, timeSeconds: number, attackPulse: PartyAttackPulse, ): number { const duration = entity.projectileSpeed > 0 ? RANGED_ATTACK_DURATION_SECONDS : ATTACK_DURATION_SECONDS return Math.max(0, Math.min(1, (timeSeconds - attackPulse.startedAt) / duration)) }