import type { Iwt2ArenaEvent, Iwt2ArenaIndicator, Iwt2EntityId, Iwt2GroundHazardState, Iwt2PartyEntityState, Iwt2Vec2, } from './types' import { applyPartyDamageInShape, createCircleIndicator, } from './mechanics' const MAX_FIRE_PUDDLES = 8 const FIRE_PUDDLE_TICK_SECONDS = 0.55 const FIRE_PUDDLE_FADE_SECONDS = 0.8 const FIRE_PUDDLE_MERGE_DISTANCE = 28 const MAX_POISON_PUDDLES = 6 const MAX_MUD_PUDDLES = 7 const MUD_PUDDLE_TICK_SECONDS = 0.22 const MUD_SLOW_SECONDS = 0.5 export function addFirePuddle({ damage, duration, hazards, nextHazardId, position, radius, sourceId, time, }: { damage: number duration: number hazards: Iwt2GroundHazardState[] nextHazardId: number position: Iwt2Vec2 radius: number sourceId: Iwt2EntityId time: number }): { hazards: Iwt2GroundHazardState[], nextHazardId: number } { return addGroundPuddle({ damage, duration, fadeSeconds: FIRE_PUDDLE_FADE_SECONDS, hazardKind: 'firePuddle', hazards, maxActive: MAX_FIRE_PUDDLES, nextHazardId, position, radius, sourceId, time, }) } export function addPoisonPuddle({ damage, duration, hazards, nextHazardId, position, radius, sourceId, time, }: { damage: number duration: number hazards: Iwt2GroundHazardState[] nextHazardId: number position: Iwt2Vec2 radius: number sourceId: Iwt2EntityId time: number }): { hazards: Iwt2GroundHazardState[], nextHazardId: number } { return addGroundPuddle({ damage, duration, fadeSeconds: 1, hazardKind: 'poisonPuddle', hazards, maxActive: MAX_POISON_PUDDLES, nextHazardId, position, radius, sourceId, time, }) } export function addMudPuddle({ duration, hazards, nextHazardId, position, radius, sourceId, time, }: { duration: number hazards: Iwt2GroundHazardState[] nextHazardId: number position: Iwt2Vec2 radius: number sourceId: Iwt2EntityId time: number }): { hazards: Iwt2GroundHazardState[], nextHazardId: number } { return addGroundPuddle({ damage: 0, duration, fadeSeconds: 0.9, hazardKind: 'mudPuddle', hazards, maxActive: MAX_MUD_PUDDLES, nextHazardId, position, radius, sourceId, time, }) } function addGroundPuddle({ damage, duration, fadeSeconds, hazardKind, hazards, maxActive, nextHazardId, position, radius, sourceId, time, }: { damage: number duration: number fadeSeconds: number hazardKind: Iwt2GroundHazardState['hazardKind'] hazards: Iwt2GroundHazardState[] maxActive: number nextHazardId: number position: Iwt2Vec2 radius: number sourceId: Iwt2EntityId time: number }): { hazards: Iwt2GroundHazardState[], nextHazardId: number } { const overlappingIndex = hazards.findIndex((hazard) => ( hazard.hazardKind === hazardKind && hazard.remainingSeconds > hazard.fadeSeconds && Math.hypot(hazard.position.x - position.x, hazard.position.y - position.y) <= FIRE_PUDDLE_MERGE_DISTANCE )) if (overlappingIndex >= 0) { return { hazards: hazards.map((hazard, index) => index === overlappingIndex ? { ...hazard, damage: Math.max(hazard.damage, damage), position: { x: (hazard.position.x + position.x) / 2, y: (hazard.position.y + position.y) / 2, }, radius: Math.max(hazard.radius, radius), remainingSeconds: Math.max(hazard.remainingSeconds, duration), } : hazard), nextHazardId, } } const nextHazard: Iwt2GroundHazardState = { id: `${hazardKind}-${nextHazardId}`, kind: 'groundHazard', hazardKind, sourceId, position: { ...position }, radius, damage, remainingSeconds: duration, fadeSeconds, nextDamageAt: time + 0.12, } return { hazards: capPuddles([...hazards, nextHazard], hazardKind, maxActive), nextHazardId: nextHazardId + 1, } } export function tickGroundHazards({ dt, hazards, party, time, }: { dt: number hazards: Iwt2GroundHazardState[] party: Iwt2PartyEntityState[] time: number }): { hazards: Iwt2GroundHazardState[] party: Iwt2PartyEntityState[] events: Iwt2ArenaEvent[] } { const events: Iwt2ArenaEvent[] = [] let nextParty = party const nextHazards: Iwt2GroundHazardState[] = [] for (const hazard of hazards) { const remainingSeconds = hazard.remainingSeconds - dt if (remainingSeconds <= 0) continue let nextDamageAt = hazard.nextDamageAt if (time >= hazard.nextDamageAt) { if (hazard.hazardKind === 'mudPuddle') { nextParty = applyMudSlow(nextParty, hazard) nextDamageAt = time + MUD_PUDDLE_TICK_SECONDS } else { const result = applyPartyDamageInShape( nextParty, { kind: 'circle', position: hazard.position, radius: hazard.radius, }, { damage: hazard.damage, damageEventType: 'groundHazardTick', sourceId: hazard.sourceId, time, }, ) nextParty = result.party events.push(...result.events) nextDamageAt = time + FIRE_PUDDLE_TICK_SECONDS } } nextHazards.push({ ...hazard, nextDamageAt, remainingSeconds, }) } return { events, hazards: nextHazards, party: nextParty, } } export function createHazardIndicators(hazards: Iwt2GroundHazardState[]): Iwt2ArenaIndicator[] { return hazards.map((hazard) => createCircleIndicator({ color: hazardColor(hazard), id: `${hazard.id}:indicator`, mechanicId: hazard.hazardKind, phase: hazard.remainingSeconds <= hazard.fadeSeconds ? 'recover' : 'active', position: hazard.position, radius: hazard.radius, sourceId: hazard.sourceId, })) } function applyMudSlow(party: Iwt2PartyEntityState[], hazard: Iwt2GroundHazardState): Iwt2PartyEntityState[] { return party.map((member) => { if (member.health <= 0) return member const distance = Math.hypot(member.position.x - hazard.position.x, member.position.y - hazard.position.y) if (distance > hazard.radius + member.radius) return member return { ...member, status: { ...member.status, slowedSeconds: Math.max(member.status.slowedSeconds, MUD_SLOW_SECONDS), }, } }) } function hazardColor(hazard: Iwt2GroundHazardState): string { if (hazard.hazardKind === 'poisonPuddle') return '#7bd84f' if (hazard.hazardKind === 'mudPuddle') return '#9a6a3a' return '#ff7a2f' } function capPuddles( hazards: Iwt2GroundHazardState[], hazardKind: Iwt2GroundHazardState['hazardKind'], maxActive: number, ): Iwt2GroundHazardState[] { let activePuddles = 0 let oldestActivePuddleIndex = -1 for (let index = 0; index < hazards.length; index += 1) { const hazard = hazards[index] if (hazard.hazardKind !== hazardKind || hazard.remainingSeconds <= hazard.fadeSeconds) continue activePuddles += 1 if (oldestActivePuddleIndex < 0) oldestActivePuddleIndex = index } if (activePuddles <= maxActive || oldestActivePuddleIndex < 0) return hazards return hazards.map((hazard, index) => index === oldestActivePuddleIndex ? { ...hazard, remainingSeconds: Math.min(hazard.remainingSeconds, hazard.fadeSeconds) } : hazard) }