143 lines
4.2 KiB
TypeScript
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))
|
|
}
|