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