Files
i-want-to-heal/src/modes/iwt2/sim/hazards.ts
T
2026-07-07 23:25:40 -04:00

304 lines
7.2 KiB
TypeScript

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