Files
i-want-to-heal/src/modes/iwt2/render/partyAnimation.ts
T
2026-07-09 23:37:31 -04:00

143 lines
4.2 KiB
TypeScript

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))
}