318 lines
6.8 KiB
TypeScript
318 lines
6.8 KiB
TypeScript
import type {
|
|
Iwt2ArenaBounds,
|
|
Iwt2ArenaCircleIndicator,
|
|
Iwt2ArenaConeIndicator,
|
|
Iwt2ArenaDonutIndicator,
|
|
Iwt2ArenaEvent,
|
|
Iwt2ArenaEventType,
|
|
Iwt2ArenaIndicator,
|
|
Iwt2ArenaIndicatorPhase,
|
|
Iwt2ArenaLaneIndicator,
|
|
Iwt2EntityId,
|
|
Iwt2PartyEntityState,
|
|
Iwt2Vec2,
|
|
} from './types'
|
|
import { circleIntersectsSegment } from './collision'
|
|
import {
|
|
addVec2,
|
|
clampVec2ToArena,
|
|
distanceVec2,
|
|
normalizeVec2,
|
|
scaleVec2,
|
|
subtractVec2,
|
|
} from './vector'
|
|
|
|
export type Iwt2LinearMovementPlan = {
|
|
direction: Iwt2Vec2
|
|
end: Iwt2Vec2
|
|
}
|
|
|
|
export type Iwt2PartyDamageEffect = {
|
|
sourceId: Iwt2EntityId
|
|
time: number
|
|
damage: number
|
|
damageEventType?: Iwt2ArenaEventType
|
|
stunSeconds?: number
|
|
knockdownSeconds?: number
|
|
statusEventType?: Iwt2ArenaEventType
|
|
excludedEntityIds?: Iwt2EntityId[]
|
|
}
|
|
|
|
export type Iwt2LaneHitShape = {
|
|
kind: 'lane'
|
|
start: Iwt2Vec2
|
|
end: Iwt2Vec2
|
|
width: number
|
|
}
|
|
|
|
export type Iwt2CircleHitShape = {
|
|
kind: 'circle'
|
|
position: Iwt2Vec2
|
|
radius: number
|
|
}
|
|
|
|
export type Iwt2DonutHitShape = {
|
|
kind: 'donut'
|
|
position: Iwt2Vec2
|
|
innerRadius: number
|
|
outerRadius: number
|
|
}
|
|
|
|
export type Iwt2HitShape = Iwt2LaneHitShape | Iwt2CircleHitShape | Iwt2DonutHitShape
|
|
|
|
export function createArenaEvent(
|
|
id: number,
|
|
time: number,
|
|
type: Iwt2ArenaEventType,
|
|
sourceId: Iwt2EntityId,
|
|
targetId?: Iwt2EntityId,
|
|
value?: number,
|
|
): Iwt2ArenaEvent {
|
|
return { id, time, type, sourceId, targetId, value }
|
|
}
|
|
|
|
export function createLinearMovementPlan({
|
|
bounds,
|
|
from,
|
|
length,
|
|
radius,
|
|
target,
|
|
}: {
|
|
bounds: Iwt2ArenaBounds
|
|
from: Iwt2Vec2
|
|
length: number
|
|
radius: number
|
|
target: Iwt2Vec2
|
|
}): Iwt2LinearMovementPlan {
|
|
const direction = normalizeVec2(subtractVec2(target, from))
|
|
const rawEnd = addVec2(from, scaleVec2(direction, length))
|
|
return {
|
|
direction,
|
|
end: clampVec2ToArena(rawEnd, radius, bounds),
|
|
}
|
|
}
|
|
|
|
export function applyPartyDamageInShape(
|
|
party: Iwt2PartyEntityState[],
|
|
shape: Iwt2HitShape,
|
|
effect: Iwt2PartyDamageEffect,
|
|
): {
|
|
party: Iwt2PartyEntityState[]
|
|
hitEntityIds: Iwt2EntityId[]
|
|
events: Iwt2ArenaEvent[]
|
|
} {
|
|
const events: Iwt2ArenaEvent[] = []
|
|
const excluded = new Set(effect.excludedEntityIds ?? [])
|
|
const hitEntityIds: Iwt2EntityId[] = []
|
|
const stunSeconds = effect.stunSeconds ?? 0
|
|
const knockdownSeconds = effect.knockdownSeconds ?? stunSeconds
|
|
const nextParty = party.map((member) => {
|
|
if (member.health <= 0 || excluded.has(member.id) || !partyMemberIntersectsShape(member, shape)) {
|
|
return member
|
|
}
|
|
|
|
hitEntityIds.push(member.id)
|
|
const shieldAbsorbed = Math.min(member.shield, effect.damage)
|
|
const healthDamage = effect.damage - shieldAbsorbed
|
|
const nextShield = member.shield - shieldAbsorbed
|
|
const nextHealth = Math.max(0, member.health - healthDamage)
|
|
events.push(createArenaEvent(
|
|
0,
|
|
effect.time,
|
|
effect.damageEventType ?? 'partyDamaged',
|
|
effect.sourceId,
|
|
member.id,
|
|
effect.damage,
|
|
))
|
|
if (stunSeconds > 0 || knockdownSeconds > 0) {
|
|
events.push(createArenaEvent(
|
|
0,
|
|
effect.time,
|
|
effect.statusEventType ?? 'partyStunned',
|
|
effect.sourceId,
|
|
member.id,
|
|
Math.max(stunSeconds, knockdownSeconds),
|
|
))
|
|
}
|
|
if (member.health > 0 && nextHealth <= 0) {
|
|
events.push(createArenaEvent(0, effect.time, 'entityDefeated', effect.sourceId, member.id))
|
|
}
|
|
|
|
return {
|
|
...member,
|
|
health: nextHealth,
|
|
shield: nextShield,
|
|
status: {
|
|
...member.status,
|
|
stunnedSeconds: Math.max(member.status.stunnedSeconds, stunSeconds),
|
|
knockedDownSeconds: Math.max(member.status.knockedDownSeconds, knockdownSeconds),
|
|
},
|
|
}
|
|
})
|
|
|
|
return { party: nextParty, hitEntityIds, events }
|
|
}
|
|
|
|
export function partyMemberIntersectsShape(member: Iwt2PartyEntityState, shape: Iwt2HitShape): boolean {
|
|
if (shape.kind === 'lane') {
|
|
return circleIntersectsSegment(
|
|
{ position: member.position, radius: member.radius },
|
|
shape.start,
|
|
shape.end,
|
|
shape.width,
|
|
)
|
|
}
|
|
const distance = distanceVec2(member.position, shape.position)
|
|
if (shape.kind === 'circle') return distance <= shape.radius + member.radius
|
|
return distance <= shape.outerRadius + member.radius && distance >= Math.max(0, shape.innerRadius - member.radius)
|
|
}
|
|
|
|
export function createLaneIndicator({
|
|
color,
|
|
end,
|
|
id,
|
|
mechanicId,
|
|
phase,
|
|
sourceId,
|
|
start,
|
|
width,
|
|
}: {
|
|
color: string
|
|
end: Iwt2Vec2
|
|
id: string
|
|
mechanicId: string
|
|
phase: Iwt2ArenaIndicatorPhase
|
|
sourceId: Iwt2EntityId
|
|
start: Iwt2Vec2
|
|
width: number
|
|
}): Iwt2ArenaLaneIndicator {
|
|
return {
|
|
color,
|
|
end: { ...end },
|
|
fillAlpha: phase === 'active' ? 0.18 : 0.11,
|
|
id,
|
|
kind: 'lane',
|
|
lineAlpha: phase === 'active' ? 0.8 : 0.45,
|
|
mechanicId,
|
|
phase,
|
|
sourceId,
|
|
start: { ...start },
|
|
width,
|
|
}
|
|
}
|
|
|
|
export function createCircleIndicator({
|
|
color,
|
|
id,
|
|
mechanicId,
|
|
phase,
|
|
position,
|
|
radius,
|
|
sourceId,
|
|
}: {
|
|
color: string
|
|
id: string
|
|
mechanicId: string
|
|
phase: Iwt2ArenaIndicatorPhase
|
|
position: Iwt2Vec2
|
|
radius: number
|
|
sourceId: Iwt2EntityId
|
|
}): Iwt2ArenaCircleIndicator {
|
|
return {
|
|
color,
|
|
fillAlpha: phase === 'active' ? 0.18 : 0.12,
|
|
id,
|
|
kind: 'circle',
|
|
lineAlpha: phase === 'active' ? 0.75 : 0.55,
|
|
mechanicId,
|
|
phase,
|
|
position: { ...position },
|
|
radius,
|
|
sourceId,
|
|
}
|
|
}
|
|
|
|
export function createConeIndicator({
|
|
angleRadians,
|
|
color,
|
|
direction,
|
|
id,
|
|
mechanicId,
|
|
origin,
|
|
phase,
|
|
range,
|
|
sourceId,
|
|
}: {
|
|
angleRadians: number
|
|
color: string
|
|
direction: Iwt2Vec2
|
|
id: string
|
|
mechanicId: string
|
|
origin: Iwt2Vec2
|
|
phase: Iwt2ArenaIndicatorPhase
|
|
range: number
|
|
sourceId: Iwt2EntityId
|
|
}): Iwt2ArenaConeIndicator {
|
|
return {
|
|
angleRadians,
|
|
color,
|
|
direction: normalizeVec2(direction),
|
|
fillAlpha: phase === 'active' ? 0.18 : 0.1,
|
|
id,
|
|
kind: 'cone',
|
|
lineAlpha: phase === 'active' ? 0.72 : 0.48,
|
|
mechanicId,
|
|
origin: { ...origin },
|
|
phase,
|
|
range,
|
|
sourceId,
|
|
}
|
|
}
|
|
|
|
export function createDonutIndicator({
|
|
color,
|
|
id,
|
|
innerRadius,
|
|
mechanicId,
|
|
outerRadius,
|
|
phase,
|
|
position,
|
|
sourceId,
|
|
}: {
|
|
color: string
|
|
id: string
|
|
innerRadius: number
|
|
mechanicId: string
|
|
outerRadius: number
|
|
phase: Iwt2ArenaIndicatorPhase
|
|
position: Iwt2Vec2
|
|
sourceId: Iwt2EntityId
|
|
}): Iwt2ArenaDonutIndicator {
|
|
return {
|
|
color,
|
|
fillAlpha: phase === 'active' ? 0.14 : 0.08,
|
|
id,
|
|
innerRadius,
|
|
kind: 'donut',
|
|
lineAlpha: phase === 'active' ? 0.76 : 0.5,
|
|
mechanicId,
|
|
outerRadius,
|
|
phase,
|
|
position: { ...position },
|
|
sourceId,
|
|
}
|
|
}
|
|
|
|
export function indicatorPhaseFromAttack(
|
|
windup: boolean,
|
|
active: boolean,
|
|
): Iwt2ArenaIndicatorPhase {
|
|
if (active) return 'active'
|
|
if (windup) return 'windup'
|
|
return 'recover'
|
|
}
|
|
|
|
export function isIndicatorActive(indicator: Iwt2ArenaIndicator): boolean {
|
|
return indicator.phase === 'active'
|
|
}
|