Files
i-want-to-heal/src/modes/iwt2/sim/mechanics.ts
T
2026-07-04 17:41:45 -04:00

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