304 lines
7.2 KiB
TypeScript
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)
|
|
}
|