Android build v1.1.15
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user