708 lines
20 KiB
TypeScript
708 lines
20 KiB
TypeScript
import { YIAN_KUT_KU_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
|
|
import type {
|
|
Iwt2ArenaEvent,
|
|
Iwt2ArenaIndicator,
|
|
Iwt2ArenaState,
|
|
Iwt2BossEntityState,
|
|
Iwt2GroundHazardState,
|
|
Iwt2HostileAddState,
|
|
Iwt2PartyEntityState,
|
|
Iwt2ProjectileEntityState,
|
|
Iwt2Vec2,
|
|
} from './types'
|
|
import {
|
|
applyPartyDamageInShape,
|
|
createCircleIndicator,
|
|
createLaneIndicator,
|
|
indicatorPhaseFromAttack,
|
|
partyMemberIntersectsShape,
|
|
} from './mechanics'
|
|
import { addFirePuddle } from './hazards'
|
|
import {
|
|
clamp,
|
|
clampVec2ToArena,
|
|
distanceVec2,
|
|
moveToward,
|
|
normalizeVec2,
|
|
scaleVec2,
|
|
subtractVec2,
|
|
withFallbackFacing,
|
|
} from './vector'
|
|
|
|
export type Iwt2YianTickResult = {
|
|
boss: Iwt2BossEntityState
|
|
party: Iwt2PartyEntityState[]
|
|
hostileAdds: Iwt2HostileAddState[]
|
|
hazards: Iwt2GroundHazardState[]
|
|
projectiles: Iwt2ProjectileEntityState[]
|
|
events: Iwt2ArenaEvent[]
|
|
indicators: Iwt2ArenaIndicator[]
|
|
nextAddId: number
|
|
nextHazardId: number
|
|
nextProjectileId: number
|
|
}
|
|
|
|
const BIRD_COUNT = 3
|
|
const BIRD_MELEE_RANGE = 24
|
|
const BIRD_MELEE_COOLDOWN = 1.15
|
|
const BIRD_FLIGHT_DAMAGE = 10
|
|
const YIAN_FIREBALL_BOUNCES = 8
|
|
const YIAN_SAFE_WALL_MARGIN = 118
|
|
const YIAN_CENTER_CAST_DISTANCE = 36
|
|
const YIAN_CENTER_CHARGE_SPEED = 430
|
|
|
|
export function tickYianKutKu(state: Iwt2ArenaState, dt: number): Iwt2YianTickResult {
|
|
const events: Iwt2ArenaEvent[] = []
|
|
const projectiles: Iwt2ProjectileEntityState[] = []
|
|
let party = state.party
|
|
let hazards = state.hazards
|
|
let nextAddId = state.nextAddId
|
|
let nextHazardId = state.nextHazardId
|
|
let nextProjectileId = state.nextProjectileId
|
|
let boss = {
|
|
...state.boss,
|
|
meleeCooldownRemaining: Math.max(0, state.boss.meleeCooldownRemaining - dt),
|
|
fireballCooldownRemaining: Math.max(0, state.boss.fireballCooldownRemaining - dt),
|
|
phaseSecondsRemaining: Math.max(0, state.boss.phaseSecondsRemaining - dt),
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
|
|
const birdResult = tickYianBirds({
|
|
dt,
|
|
hazards,
|
|
nextHazardId,
|
|
party,
|
|
state,
|
|
time: state.time + dt,
|
|
})
|
|
party = birdResult.party
|
|
hazards = birdResult.hazards
|
|
nextHazardId = birdResult.nextHazardId
|
|
events.push(...birdResult.events)
|
|
let hostileAdds = birdResult.hostileAdds
|
|
|
|
const target = getBossTarget(party)
|
|
if (boss.health <= 0 || !target) {
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
if (boss.attackPhase === 'relocating') {
|
|
const nextPosition = clampVec2ToArena(
|
|
moveToward(boss.position, boss.relocateTarget, YIAN_CENTER_CHARGE_SPEED * dt),
|
|
boss.radius,
|
|
state.bounds,
|
|
)
|
|
boss = {
|
|
...boss,
|
|
position: nextPosition,
|
|
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
|
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
|
}
|
|
if (distanceVec2(nextPosition, boss.relocateTarget) <= 4 || boss.phaseSecondsRemaining <= 0) {
|
|
boss = {
|
|
...boss,
|
|
attackPhase: 'idle',
|
|
phaseSecondsRemaining: 0,
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
if (boss.attackPhase === 'fireballWindup') {
|
|
boss = {
|
|
...boss,
|
|
facing: withFallbackFacing(subtractVec2(boss.fireballTarget, boss.position), boss.facing),
|
|
}
|
|
if (boss.phaseSecondsRemaining <= 0) {
|
|
const direction = withFallbackFacing(subtractVec2(boss.fireballTarget, boss.position), boss.facing)
|
|
projectiles.push({
|
|
id: `boss-projectile-${nextProjectileId}`,
|
|
sourceId: boss.id,
|
|
owner: 'boss',
|
|
projectileKind: 'fireball',
|
|
color: '#ff8b2b',
|
|
position: {
|
|
x: boss.position.x + direction.x * (boss.radius + YIAN_KUT_KU_BOSS_METADATA.fireballRadius! + 2),
|
|
y: boss.position.y + direction.y * (boss.radius + YIAN_KUT_KU_BOSS_METADATA.fireballRadius! + 2),
|
|
},
|
|
velocity: scaleVec2(direction, YIAN_KUT_KU_BOSS_METADATA.fireballSpeed!),
|
|
radius: YIAN_KUT_KU_BOSS_METADATA.fireballRadius!,
|
|
damage: YIAN_KUT_KU_BOSS_METADATA.fireballDamage!,
|
|
remainingSeconds: 7,
|
|
bouncesRemaining: YIAN_FIREBALL_BOUNCES,
|
|
})
|
|
nextProjectileId += 1
|
|
boss = {
|
|
...boss,
|
|
attackPhase: 'fireballRecover',
|
|
phaseSecondsRemaining: 0.35,
|
|
}
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
if (boss.attackPhase === 'fireballRecover' || boss.attackPhase === 'birdSummonRecover') {
|
|
if (boss.phaseSecondsRemaining <= 0) {
|
|
boss = { ...boss, attackPhase: 'idle', phaseSecondsRemaining: 0 }
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
if (shouldYianRelocate(boss, state)) {
|
|
const center = yianCenterTarget(state)
|
|
boss = {
|
|
...boss,
|
|
attackPhase: 'relocating',
|
|
phaseSecondsRemaining: 1.4,
|
|
relocateTarget: center,
|
|
chargeStart: { ...boss.position },
|
|
chargeEnd: center,
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
const nextThreshold = nextBirdWaveThreshold(boss)
|
|
if (nextThreshold !== undefined && boss.health / boss.maxHealth <= nextThreshold) {
|
|
const spawn = spawnBirdWave(state, nextAddId, boss.id, nextThreshold)
|
|
hostileAdds = [...hostileAdds, ...spawn.hostileAdds]
|
|
nextAddId = spawn.nextAddId
|
|
boss = {
|
|
...boss,
|
|
attackPhase: 'birdSummonRecover',
|
|
birdWaveThresholdsTriggered: [...boss.birdWaveThresholdsTriggered, nextThreshold],
|
|
phaseSecondsRemaining: 0.55,
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
if (boss.fireballCooldownRemaining <= 0) {
|
|
if (!isYianAtCenter(boss, state)) {
|
|
const center = yianCenterTarget(state)
|
|
boss = {
|
|
...boss,
|
|
attackPhase: 'relocating',
|
|
phaseSecondsRemaining: 1.4,
|
|
relocateTarget: center,
|
|
chargeStart: { ...boss.position },
|
|
chargeEnd: center,
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
boss = {
|
|
...boss,
|
|
attackPhase: 'fireballWindup',
|
|
fireballCooldownRemaining: YIAN_KUT_KU_BOSS_METADATA.fireballCooldown!,
|
|
fireballTarget: { ...target.position },
|
|
phaseSecondsRemaining: YIAN_KUT_KU_BOSS_METADATA.fireballWindup!,
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
const meleeResult = maybeApplyMelee(party, boss, target, state.time + dt)
|
|
party = meleeResult.party
|
|
boss = meleeResult.boss
|
|
events.push(...meleeResult.events)
|
|
if (meleeResult.events.length > 0) {
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
const nextPosition = clampVec2ToArena(
|
|
moveToward(boss.position, yianApproachTarget(target, state), YIAN_KUT_KU_BOSS_METADATA.moveSpeed * dt),
|
|
boss.radius,
|
|
state.bounds,
|
|
)
|
|
boss = {
|
|
...boss,
|
|
position: nextPosition,
|
|
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
|
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
|
}
|
|
return withYianIndicators({
|
|
boss,
|
|
events,
|
|
hazards,
|
|
hostileAdds,
|
|
nextAddId,
|
|
nextHazardId,
|
|
nextProjectileId,
|
|
party,
|
|
projectiles,
|
|
})
|
|
}
|
|
|
|
function shouldYianRelocate(
|
|
boss: Iwt2BossEntityState,
|
|
state: Iwt2ArenaState,
|
|
): boolean {
|
|
return (
|
|
boss.position.x < YIAN_SAFE_WALL_MARGIN
|
|
|| boss.position.x > state.bounds.width - YIAN_SAFE_WALL_MARGIN
|
|
|| boss.position.y < YIAN_SAFE_WALL_MARGIN
|
|
|| boss.position.y > state.bounds.height - YIAN_SAFE_WALL_MARGIN
|
|
)
|
|
}
|
|
|
|
function isYianAtCenter(boss: Iwt2BossEntityState, state: Iwt2ArenaState): boolean {
|
|
return distanceVec2(boss.position, yianCenterTarget(state)) <= YIAN_CENTER_CAST_DISTANCE
|
|
}
|
|
|
|
function yianApproachTarget(target: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 {
|
|
return clampVec2ToArena({
|
|
x: target.position.x + (target.position.x < state.bounds.width * 0.5 ? 190 : -190),
|
|
y: target.position.y + (target.position.y < state.bounds.height * 0.5 ? 96 : -96),
|
|
}, YIAN_KUT_KU_BOSS_METADATA.radius, state.bounds)
|
|
}
|
|
|
|
function yianCenterTarget(state: Iwt2ArenaState): Iwt2Vec2 {
|
|
return {
|
|
x: state.bounds.width * 0.5,
|
|
y: state.bounds.height * 0.5,
|
|
}
|
|
}
|
|
|
|
function tickYianBirds({
|
|
dt,
|
|
hazards,
|
|
nextHazardId,
|
|
party,
|
|
state,
|
|
time,
|
|
}: {
|
|
dt: number
|
|
hazards: Iwt2GroundHazardState[]
|
|
nextHazardId: number
|
|
party: Iwt2PartyEntityState[]
|
|
state: Iwt2ArenaState
|
|
time: number
|
|
}): {
|
|
hostileAdds: Iwt2HostileAddState[]
|
|
party: Iwt2PartyEntityState[]
|
|
hazards: Iwt2GroundHazardState[]
|
|
nextHazardId: number
|
|
events: Iwt2ArenaEvent[]
|
|
} {
|
|
const events: Iwt2ArenaEvent[] = []
|
|
const hostileAdds: Iwt2HostileAddState[] = []
|
|
let nextParty = party
|
|
let nextHazards = hazards
|
|
let nextHazardIdValue = nextHazardId
|
|
|
|
for (const add of state.hostileAdds) {
|
|
if (add.health <= 0) continue
|
|
let nextAdd = {
|
|
...add,
|
|
attackCooldownRemaining: Math.max(0, add.attackCooldownRemaining - dt),
|
|
flightCooldownRemaining: Math.max(0, add.flightCooldownRemaining - dt),
|
|
phaseSecondsRemaining: Math.max(0, add.phaseSecondsRemaining - dt),
|
|
}
|
|
|
|
if (nextAdd.attackPhase === 'flightWindup') {
|
|
if (nextAdd.phaseSecondsRemaining <= 0) {
|
|
nextAdd = {
|
|
...nextAdd,
|
|
attackPhase: 'flying',
|
|
position: { ...nextAdd.flightStart },
|
|
velocity: scaleVec2(normalizeVec2(subtractVec2(nextAdd.flightEnd, nextAdd.flightStart)), YIAN_KUT_KU_BOSS_METADATA.birdFlightSpeed!),
|
|
}
|
|
}
|
|
hostileAdds.push(nextAdd)
|
|
continue
|
|
}
|
|
|
|
if (nextAdd.attackPhase === 'flying') {
|
|
const previousPosition = nextAdd.position
|
|
const nextPosition = moveToward(
|
|
nextAdd.position,
|
|
nextAdd.flightEnd,
|
|
YIAN_KUT_KU_BOSS_METADATA.birdFlightSpeed! * dt,
|
|
)
|
|
const shape = {
|
|
kind: 'lane' as const,
|
|
start: previousPosition,
|
|
end: nextPosition,
|
|
width: nextAdd.radius,
|
|
}
|
|
const hitMembers = nextParty.filter((member) => (
|
|
!nextAdd.flightHitEntityIds.includes(member.id)
|
|
&& partyMemberIntersectsShape(member, shape)
|
|
))
|
|
const hitResult = applyPartyDamageInShape(nextParty, shape, {
|
|
damage: BIRD_FLIGHT_DAMAGE,
|
|
excludedEntityIds: nextAdd.flightHitEntityIds,
|
|
knockdownSeconds: 0,
|
|
sourceId: nextAdd.id,
|
|
stunSeconds: YIAN_KUT_KU_BOSS_METADATA.birdStunSeconds!,
|
|
time,
|
|
})
|
|
nextParty = hitResult.party
|
|
events.push(...hitResult.events)
|
|
for (const member of hitMembers) {
|
|
const puddle = addFirePuddle({
|
|
damage: YIAN_KUT_KU_BOSS_METADATA.firePuddleDamage!,
|
|
duration: YIAN_KUT_KU_BOSS_METADATA.firePuddleSeconds!,
|
|
hazards: nextHazards,
|
|
nextHazardId: nextHazardIdValue,
|
|
position: member.position,
|
|
radius: YIAN_KUT_KU_BOSS_METADATA.firePuddleRadius!,
|
|
sourceId: nextAdd.id,
|
|
time,
|
|
})
|
|
nextHazards = puddle.hazards
|
|
nextHazardIdValue = puddle.nextHazardId
|
|
}
|
|
|
|
nextAdd = {
|
|
...nextAdd,
|
|
flightHitEntityIds: [...nextAdd.flightHitEntityIds, ...hitResult.hitEntityIds],
|
|
position: nextPosition,
|
|
velocity: scaleVec2(subtractVec2(nextPosition, previousPosition), dt > 0 ? 1 / dt : 0),
|
|
facing: { x: 1, y: 0 },
|
|
}
|
|
if (distanceVec2(nextPosition, nextAdd.flightEnd) <= 1) {
|
|
nextAdd = {
|
|
...nextAdd,
|
|
attackPhase: 'flightRecover',
|
|
flightCooldownRemaining: YIAN_KUT_KU_BOSS_METADATA.birdFlightCooldown!,
|
|
phaseSecondsRemaining: 0.4,
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
}
|
|
hostileAdds.push(nextAdd)
|
|
continue
|
|
}
|
|
|
|
if (nextAdd.attackPhase === 'flightRecover') {
|
|
if (nextAdd.phaseSecondsRemaining <= 0) {
|
|
nextAdd = { ...nextAdd, attackPhase: 'idle', phaseSecondsRemaining: 0 }
|
|
}
|
|
hostileAdds.push(nextAdd)
|
|
continue
|
|
}
|
|
|
|
if (nextAdd.flightCooldownRemaining <= 0) {
|
|
hostileAdds.push(startBirdFlight(nextAdd, state, time))
|
|
continue
|
|
}
|
|
|
|
const target = getNearestPartyMember(nextParty, nextAdd.position)
|
|
if (!target) {
|
|
hostileAdds.push({ ...nextAdd, velocity: { x: 0, y: 0 } })
|
|
continue
|
|
}
|
|
|
|
const nextPosition = clampVec2ToArena(
|
|
moveToward(nextAdd.position, target.position, YIAN_KUT_KU_BOSS_METADATA.moveSpeed * 1.35 * dt),
|
|
nextAdd.radius,
|
|
state.bounds,
|
|
)
|
|
nextAdd = {
|
|
...nextAdd,
|
|
position: nextPosition,
|
|
velocity: scaleVec2(subtractVec2(nextPosition, nextAdd.position), dt > 0 ? 1 / dt : 0),
|
|
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), nextAdd.facing),
|
|
}
|
|
if (
|
|
nextAdd.attackCooldownRemaining <= 0
|
|
&& distanceVec2(nextAdd.position, target.position) <= nextAdd.radius + target.radius + BIRD_MELEE_RANGE
|
|
) {
|
|
const hitResult = applyPartyDamageInShape(nextParty, {
|
|
kind: 'circle',
|
|
position: nextAdd.position,
|
|
radius: nextAdd.radius + BIRD_MELEE_RANGE,
|
|
}, {
|
|
damage: YIAN_KUT_KU_BOSS_METADATA.birdContactDamage!,
|
|
sourceId: nextAdd.id,
|
|
time,
|
|
})
|
|
nextParty = hitResult.party
|
|
events.push(...hitResult.events)
|
|
nextAdd = { ...nextAdd, attackCooldownRemaining: BIRD_MELEE_COOLDOWN }
|
|
}
|
|
hostileAdds.push(nextAdd)
|
|
}
|
|
|
|
return {
|
|
events,
|
|
hazards: nextHazards,
|
|
hostileAdds,
|
|
nextHazardId: nextHazardIdValue,
|
|
party: nextParty,
|
|
}
|
|
}
|
|
|
|
function spawnBirdWave(
|
|
state: Iwt2ArenaState,
|
|
nextAddId: number,
|
|
sourceBossId: Iwt2BossId,
|
|
threshold: number,
|
|
): { hostileAdds: Iwt2HostileAddState[], nextAddId: number } {
|
|
const hostileAdds: Iwt2HostileAddState[] = []
|
|
let nextId = nextAddId
|
|
for (let index = 0; index < BIRD_COUNT; index += 1) {
|
|
const y = birdLaneY(state, index, threshold * 100)
|
|
const radius = YIAN_KUT_KU_BOSS_METADATA.birdRadius!
|
|
const flightStart = { x: -radius - 8, y }
|
|
const flightEnd = { x: state.bounds.width + radius + 8, y }
|
|
hostileAdds.push({
|
|
id: `yian-bird-${nextId}`,
|
|
kind: 'hostileAdd',
|
|
addKind: 'yian-bird',
|
|
sourceBossId,
|
|
position: { ...flightStart },
|
|
velocity: { x: 0, y: 0 },
|
|
facing: { x: 1, y: 0 },
|
|
radius,
|
|
health: YIAN_KUT_KU_BOSS_METADATA.birdHealth!,
|
|
maxHealth: YIAN_KUT_KU_BOSS_METADATA.birdHealth!,
|
|
damageDone: 0,
|
|
attackCooldownRemaining: 0,
|
|
flightCooldownRemaining: YIAN_KUT_KU_BOSS_METADATA.birdFlightCooldown!,
|
|
attackPhase: 'flightWindup',
|
|
phaseSecondsRemaining: YIAN_KUT_KU_BOSS_METADATA.birdFlightWindup!,
|
|
flightStart,
|
|
flightEnd,
|
|
flightHitEntityIds: [],
|
|
})
|
|
nextId += 1
|
|
}
|
|
return { hostileAdds, nextAddId: nextId }
|
|
}
|
|
|
|
function startBirdFlight(add: Iwt2HostileAddState, state: Iwt2ArenaState, time: number): Iwt2HostileAddState {
|
|
const y = birdLaneY(state, Number(add.id.replace(/\D/g, '')) % BIRD_COUNT, time)
|
|
const flightStart = { x: -add.radius - 8, y }
|
|
const flightEnd = { x: state.bounds.width + add.radius + 8, y }
|
|
return {
|
|
...add,
|
|
attackPhase: 'flightWindup',
|
|
phaseSecondsRemaining: YIAN_KUT_KU_BOSS_METADATA.birdFlightWindup!,
|
|
flightStart,
|
|
flightEnd,
|
|
flightHitEntityIds: [],
|
|
velocity: { x: 0, y: 0 },
|
|
}
|
|
}
|
|
|
|
function birdLaneY(state: Iwt2ArenaState, index: number, seed: number): number {
|
|
const laneHeight = state.bounds.height / (BIRD_COUNT + 1)
|
|
const base = laneHeight * (index + 1)
|
|
const wobble = Math.sin(seed * 8.37 + index * 2.1) * 34
|
|
return clamp(base + wobble, 58, state.bounds.height - 58)
|
|
}
|
|
|
|
function nextBirdWaveThreshold(boss: Iwt2BossEntityState): number | undefined {
|
|
return YIAN_KUT_KU_BOSS_METADATA.birdWaveThresholds
|
|
?.find((threshold) => !boss.birdWaveThresholdsTriggered.includes(threshold))
|
|
}
|
|
|
|
function getBossTarget(party: Iwt2PartyEntityState[]): Iwt2PartyEntityState | undefined {
|
|
const livingTank = party.find((member) => member.classId === 'paladin' && member.health > 0)
|
|
if (livingTank) return livingTank
|
|
return party.find((member) => member.health > 0)
|
|
}
|
|
|
|
function getNearestPartyMember(party: Iwt2PartyEntityState[], position: Iwt2Vec2): Iwt2PartyEntityState | undefined {
|
|
let nearest: Iwt2PartyEntityState | undefined
|
|
let nearestDistance = Number.POSITIVE_INFINITY
|
|
for (const member of party) {
|
|
if (member.health <= 0) continue
|
|
const distance = distanceVec2(member.position, position)
|
|
if (distance < nearestDistance) {
|
|
nearest = member
|
|
nearestDistance = distance
|
|
}
|
|
}
|
|
return nearest
|
|
}
|
|
|
|
function maybeApplyMelee(
|
|
party: Iwt2PartyEntityState[],
|
|
boss: Iwt2BossEntityState,
|
|
target: Iwt2PartyEntityState,
|
|
time: number,
|
|
): { boss: Iwt2BossEntityState, party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
|
if (boss.meleeCooldownRemaining > 0) return { boss, party, events: [] }
|
|
if (distanceVec2(boss.position, target.position) > YIAN_KUT_KU_BOSS_METADATA.meleeRange + target.radius) {
|
|
return { boss, party, events: [] }
|
|
}
|
|
const result = applyPartyDamageInShape(party, {
|
|
kind: 'circle',
|
|
position: boss.position,
|
|
radius: YIAN_KUT_KU_BOSS_METADATA.meleeRange,
|
|
}, {
|
|
damage: YIAN_KUT_KU_BOSS_METADATA.meleeDamage,
|
|
sourceId: boss.id,
|
|
time,
|
|
})
|
|
return {
|
|
boss: { ...boss, meleeCooldownRemaining: YIAN_KUT_KU_BOSS_METADATA.meleeCooldown },
|
|
party: result.party,
|
|
events: result.events,
|
|
}
|
|
}
|
|
|
|
function withYianIndicators(result: Omit<Iwt2YianTickResult, 'indicators'>): Iwt2YianTickResult {
|
|
return {
|
|
...result,
|
|
indicators: createYianIndicators(result.boss, result.hostileAdds),
|
|
}
|
|
}
|
|
|
|
function createYianIndicators(
|
|
boss: Iwt2BossEntityState,
|
|
hostileAdds: Iwt2HostileAddState[],
|
|
): Iwt2ArenaIndicator[] {
|
|
const indicators: Iwt2ArenaIndicator[] = []
|
|
if (boss.attackPhase === 'relocating') {
|
|
indicators.push(createLaneIndicator({
|
|
color: '#ffce5c',
|
|
end: boss.chargeEnd,
|
|
id: `${boss.id}:center-charge`,
|
|
mechanicId: 'yian-center-charge',
|
|
phase: 'active',
|
|
sourceId: boss.id,
|
|
start: boss.chargeStart,
|
|
width: boss.radius,
|
|
}))
|
|
}
|
|
if (boss.attackPhase === 'fireballWindup') {
|
|
indicators.push(createCircleIndicator({
|
|
color: '#ff8b2b',
|
|
id: `${boss.id}:fireball-cast`,
|
|
mechanicId: 'yian-fireball-cast',
|
|
phase: 'windup',
|
|
position: boss.position,
|
|
radius: boss.radius + 18,
|
|
sourceId: boss.id,
|
|
}))
|
|
indicators.push(createLaneIndicator({
|
|
color: '#ffb347',
|
|
end: boss.fireballTarget,
|
|
id: `${boss.id}:fireball-line`,
|
|
mechanicId: 'yian-fireball-line',
|
|
phase: 'windup',
|
|
sourceId: boss.id,
|
|
start: boss.position,
|
|
width: YIAN_KUT_KU_BOSS_METADATA.fireballRadius! + 4,
|
|
}))
|
|
}
|
|
for (const add of hostileAdds) {
|
|
if (
|
|
add.attackPhase !== 'flightWindup'
|
|
&& add.attackPhase !== 'flying'
|
|
&& add.attackPhase !== 'flightRecover'
|
|
) {
|
|
continue
|
|
}
|
|
indicators.push(createLaneIndicator({
|
|
color: '#ffce5c',
|
|
end: add.flightEnd,
|
|
id: `${add.id}:flight-lane`,
|
|
mechanicId: 'yian-bird-flight',
|
|
phase: indicatorPhaseFromAttack(
|
|
add.attackPhase === 'flightWindup',
|
|
add.attackPhase === 'flying',
|
|
),
|
|
sourceId: add.id,
|
|
start: add.flightStart,
|
|
width: add.radius,
|
|
}))
|
|
}
|
|
return indicators
|
|
}
|