Android build v1.1.22
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
import type { Iwt2BossTickResult } from './bossAi'
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2ArenaIndicator,
|
||||
Iwt2ArenaIndicatorPhase,
|
||||
Iwt2ArenaState,
|
||||
Iwt2BossEntityState,
|
||||
Iwt2MechanicCircleState,
|
||||
Iwt2MechanicLaneState,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2Vec2,
|
||||
} from './types'
|
||||
import {
|
||||
applyPartyDamageInShape,
|
||||
createCircleIndicator,
|
||||
createConeIndicator,
|
||||
createLaneIndicator,
|
||||
indicatorPhaseFromAttack,
|
||||
} from './mechanics'
|
||||
import {
|
||||
addVec2,
|
||||
clamp,
|
||||
clampVec2ToArena,
|
||||
distanceVec2,
|
||||
moveToward,
|
||||
normalizeVec2,
|
||||
scaleVec2,
|
||||
subtractVec2,
|
||||
withFallbackFacing,
|
||||
} from './vector'
|
||||
|
||||
type RimebastionAttackPhase =
|
||||
| Iwt2BossEntityState['attackPhase']
|
||||
| 'iceWallWindup'
|
||||
| 'shardShatterWindup'
|
||||
| 'shardShatterRecover'
|
||||
|
||||
type RimebastionBossState = Omit<Iwt2BossEntityState, 'attackPhase'> & {
|
||||
attackPhase: RimebastionAttackPhase
|
||||
}
|
||||
|
||||
type ShardCone = {
|
||||
id: string
|
||||
origin: Iwt2Vec2
|
||||
direction: Iwt2Vec2
|
||||
range: number
|
||||
angleRadians: number
|
||||
}
|
||||
|
||||
const RIMEBASTION_BALANCE = {
|
||||
iceWallCooldown: 7.2,
|
||||
iceWallDamage: 6,
|
||||
iceWallStunSeconds: 0.25,
|
||||
iceWallWindup: 1,
|
||||
iceCircleCount: 2,
|
||||
iceCircleRadius: 58,
|
||||
iceWallLaneCount: 3,
|
||||
iceWallWidth: 22,
|
||||
meleeCooldown: 1.45,
|
||||
meleeDamage: 4,
|
||||
meleeRange: 66,
|
||||
moveSpeed: 64,
|
||||
relocateCooldownFloor: 1.3,
|
||||
relocateSpeed: 112,
|
||||
relocateSeconds: 1.6,
|
||||
shatterConeAngleRadians: Math.PI * 0.34,
|
||||
shatterConeDamage: 9,
|
||||
shatterConeRange: 158,
|
||||
shatterLaneDamage: 11,
|
||||
shatterLaneWidth: 34,
|
||||
shatterRecover: 0.52,
|
||||
shatterStunSeconds: 0.4,
|
||||
shatterWindup: 0.72,
|
||||
wallDisengageSeconds: 0.7,
|
||||
wallMargin: 68,
|
||||
}
|
||||
|
||||
const WALL_COLOR = '#a7ecff'
|
||||
const SHARD_COLOR = '#e8fbff'
|
||||
|
||||
export function tickRimebastion(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
let party = state.party
|
||||
let boss: RimebastionBossState = {
|
||||
...state.boss,
|
||||
meleeCooldownRemaining: Math.max(0, state.boss.meleeCooldownRemaining - dt),
|
||||
chargeCooldownRemaining: Math.max(0, state.boss.chargeCooldownRemaining - dt),
|
||||
phaseSecondsRemaining: Math.max(0, state.boss.phaseSecondsRemaining - dt),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
boss = {
|
||||
...boss,
|
||||
wallContactSeconds: isBossNearWall(boss, state)
|
||||
? boss.wallContactSeconds + dt
|
||||
: Math.max(0, boss.wallContactSeconds - dt * 2),
|
||||
}
|
||||
const target = getBossTarget(party)
|
||||
|
||||
if (boss.health <= 0 || !target) return withRimebastionIndicators({ boss, party, events, state })
|
||||
|
||||
if (boss.attackPhase === 'relocating') {
|
||||
boss = moveBossTowardRelocationTarget(boss, state, target, dt)
|
||||
if (boss.phaseSecondsRemaining <= 0 || distanceVec2(boss.position, boss.relocateTarget) <= 8) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
chargeCooldownRemaining: Math.max(boss.chargeCooldownRemaining, RIMEBASTION_BALANCE.relocateCooldownFloor),
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'iceWallWindup') {
|
||||
boss = {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, boss.position), boss.facing),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const result = applyIceWalls(party, boss, state.time + dt)
|
||||
party = result.party
|
||||
events.push(...result.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'shardShatterWindup',
|
||||
phaseSecondsRemaining: RIMEBASTION_BALANCE.shatterWindup,
|
||||
}
|
||||
}
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'shardShatterWindup') {
|
||||
boss = {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, boss.position), boss.facing),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const result = applyShardShatter(party, boss, state, state.time + dt)
|
||||
party = result.party
|
||||
events.push(...result.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'shardShatterRecover',
|
||||
phaseSecondsRemaining: RIMEBASTION_BALANCE.shatterRecover,
|
||||
}
|
||||
}
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'shardShatterRecover') {
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
}
|
||||
}
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'idle' && boss.wallContactSeconds >= RIMEBASTION_BALANCE.wallDisengageSeconds) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'relocating',
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: RIMEBASTION_BALANCE.relocateSeconds,
|
||||
relocateTarget: relocationTarget(boss, state),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.chargeCooldownRemaining <= 0) {
|
||||
const pattern = createIceWallPattern(state, party, boss)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'iceWallWindup',
|
||||
chargeCooldownRemaining: RIMEBASTION_BALANCE.iceWallCooldown,
|
||||
chargeCount: boss.chargeCount + 1,
|
||||
mechanicCircles: pattern.circles,
|
||||
mechanicLanes: pattern.lanes,
|
||||
phaseSecondsRemaining: RIMEBASTION_BALANCE.iceWallWindup,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
const meleeResult = maybeApplyMelee(party, boss, target, state.time + dt)
|
||||
party = meleeResult.party
|
||||
events.push(...meleeResult.events)
|
||||
boss = meleeResult.boss
|
||||
if (events.length > 0) return withRimebastionIndicators({ boss, party, events, state })
|
||||
|
||||
boss = moveBossTowardEngagePoint(boss, state, target, dt)
|
||||
return withRimebastionIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
function createIceWallPattern(
|
||||
state: Iwt2ArenaState,
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: RimebastionBossState,
|
||||
): { lanes: Iwt2MechanicLaneState[], circles: Iwt2MechanicCircleState[] } {
|
||||
const living = party.filter((member) => member.health > 0)
|
||||
const center = partyCenter(living, arenaCenter(state))
|
||||
const vertical = boss.chargeCount % 2 === 0
|
||||
const lanes: Iwt2MechanicLaneState[] = []
|
||||
const offsets = [-96, 0, 96]
|
||||
|
||||
for (let index = 0; index < RIMEBASTION_BALANCE.iceWallLaneCount; index += 1) {
|
||||
const offset = offsets[index] ?? 0
|
||||
if (vertical) {
|
||||
const x = clamp(center.x + offset, 82, state.bounds.width - 82)
|
||||
lanes.push({
|
||||
end: { x, y: state.bounds.height - 38 },
|
||||
id: `rime-wall-lane-${index}`,
|
||||
start: { x, y: 38 },
|
||||
width: RIMEBASTION_BALANCE.iceWallWidth,
|
||||
})
|
||||
} else {
|
||||
const y = clamp(center.y + offset * 0.72, 74, state.bounds.height - 74)
|
||||
lanes.push({
|
||||
end: { x: state.bounds.width - 38, y },
|
||||
id: `rime-wall-lane-${index}`,
|
||||
start: { x: 38, y },
|
||||
width: RIMEBASTION_BALANCE.iceWallWidth,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const circles = living
|
||||
.filter((member) => member.aiRole === 'ranged' || member.aiRole === 'flanker' || member.aiRole === 'player')
|
||||
.slice(0, RIMEBASTION_BALANCE.iceCircleCount)
|
||||
.map((member, index) => ({
|
||||
id: `rime-wall-circle-${index}`,
|
||||
position: clampVec2ToArena(member.position, RIMEBASTION_BALANCE.iceCircleRadius, state.bounds),
|
||||
radius: RIMEBASTION_BALANCE.iceCircleRadius,
|
||||
}))
|
||||
|
||||
return { circles, lanes }
|
||||
}
|
||||
|
||||
function applyIceWalls(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: RimebastionBossState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
let nextParty = party
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const hitEntityIds: string[] = []
|
||||
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
kind: 'lane',
|
||||
end: lane.end,
|
||||
start: lane.start,
|
||||
width: lane.width,
|
||||
}, {
|
||||
damage: RIMEBASTION_BALANCE.iceWallDamage,
|
||||
excludedEntityIds: hitEntityIds,
|
||||
knockdownSeconds: 0,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: RIMEBASTION_BALANCE.iceWallStunSeconds,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
hitEntityIds.push(...result.hitEntityIds)
|
||||
events.push(...result.events)
|
||||
}
|
||||
|
||||
for (const circle of boss.mechanicCircles) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
kind: 'circle',
|
||||
position: circle.position,
|
||||
radius: circle.radius,
|
||||
}, {
|
||||
damage: RIMEBASTION_BALANCE.iceWallDamage,
|
||||
excludedEntityIds: hitEntityIds,
|
||||
knockdownSeconds: 0,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: RIMEBASTION_BALANCE.iceWallStunSeconds,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
hitEntityIds.push(...result.hitEntityIds)
|
||||
events.push(...result.events)
|
||||
}
|
||||
|
||||
return { party: nextParty, events }
|
||||
}
|
||||
|
||||
function applyShardShatter(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: RimebastionBossState,
|
||||
state: Iwt2ArenaState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
let nextParty = party
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const hitEntityIds: string[] = []
|
||||
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
kind: 'lane',
|
||||
end: lane.end,
|
||||
start: lane.start,
|
||||
width: RIMEBASTION_BALANCE.shatterLaneWidth,
|
||||
}, {
|
||||
damage: RIMEBASTION_BALANCE.shatterLaneDamage,
|
||||
excludedEntityIds: hitEntityIds,
|
||||
knockdownSeconds: RIMEBASTION_BALANCE.shatterStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: RIMEBASTION_BALANCE.shatterStunSeconds,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
hitEntityIds.push(...result.hitEntityIds)
|
||||
events.push(...result.events)
|
||||
}
|
||||
|
||||
for (const cone of createShardCones(boss, state)) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
kind: 'cone',
|
||||
angleRadians: cone.angleRadians,
|
||||
direction: cone.direction,
|
||||
origin: cone.origin,
|
||||
range: cone.range,
|
||||
}, {
|
||||
damage: RIMEBASTION_BALANCE.shatterConeDamage,
|
||||
excludedEntityIds: hitEntityIds,
|
||||
knockdownSeconds: RIMEBASTION_BALANCE.shatterStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: RIMEBASTION_BALANCE.shatterStunSeconds,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
hitEntityIds.push(...result.hitEntityIds)
|
||||
events.push(...result.events)
|
||||
}
|
||||
|
||||
return { party: nextParty, events }
|
||||
}
|
||||
|
||||
function createShardCones(boss: RimebastionBossState, state: Iwt2ArenaState): ShardCone[] {
|
||||
const cones: ShardCone[] = []
|
||||
const center = arenaCenter(state)
|
||||
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
const midpoint = {
|
||||
x: (lane.start.x + lane.end.x) * 0.5,
|
||||
y: (lane.start.y + lane.end.y) * 0.5,
|
||||
}
|
||||
cones.push({
|
||||
angleRadians: RIMEBASTION_BALANCE.shatterConeAngleRadians,
|
||||
direction: withFallbackFacing(subtractVec2(center, midpoint), boss.facing),
|
||||
id: `${lane.id}-shard-cone`,
|
||||
origin: midpoint,
|
||||
range: RIMEBASTION_BALANCE.shatterConeRange,
|
||||
})
|
||||
}
|
||||
|
||||
for (const circle of boss.mechanicCircles) {
|
||||
cones.push({
|
||||
angleRadians: RIMEBASTION_BALANCE.shatterConeAngleRadians,
|
||||
direction: withFallbackFacing(subtractVec2(circle.position, boss.position), boss.facing),
|
||||
id: `${circle.id}-shard-cone`,
|
||||
origin: circle.position,
|
||||
range: RIMEBASTION_BALANCE.shatterConeRange * 0.86,
|
||||
})
|
||||
}
|
||||
|
||||
return cones
|
||||
}
|
||||
|
||||
function moveBossTowardRelocationTarget(
|
||||
boss: RimebastionBossState,
|
||||
state: Iwt2ArenaState,
|
||||
target: Iwt2PartyEntityState,
|
||||
dt: number,
|
||||
): RimebastionBossState {
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, boss.relocateTarget, RIMEBASTION_BALANCE.relocateSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
wallContactSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function moveBossTowardEngagePoint(
|
||||
boss: RimebastionBossState,
|
||||
state: Iwt2ArenaState,
|
||||
target: Iwt2PartyEntityState,
|
||||
dt: number,
|
||||
): RimebastionBossState {
|
||||
const center = arenaCenter(state)
|
||||
const targetNearWall = isPositionNearWall(target.position, target.radius + RIMEBASTION_BALANCE.wallMargin, state)
|
||||
const desired = targetNearWall
|
||||
? addVec2(target.position, scaleVec2(normalizeVec2(subtractVec2(center, target.position)), RIMEBASTION_BALANCE.meleeRange * 0.9))
|
||||
: target.position
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, desired, RIMEBASTION_BALANCE.moveSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
}
|
||||
}
|
||||
|
||||
function relocationTarget(boss: RimebastionBossState, state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
const center = arenaCenter(state)
|
||||
const awayFromWall = normalizeVec2({
|
||||
x: boss.position.x < state.bounds.width * 0.5 ? 1 : -1,
|
||||
y: boss.position.y < state.bounds.height * 0.5 ? 1 : -1,
|
||||
})
|
||||
return clampVec2ToArena(addVec2(center, scaleVec2(awayFromWall, 58)), boss.radius, state.bounds)
|
||||
}
|
||||
|
||||
function maybeApplyMelee(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: RimebastionBossState,
|
||||
target: Iwt2PartyEntityState,
|
||||
time: number,
|
||||
): { boss: RimebastionBossState, party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
if (boss.meleeCooldownRemaining > 0) return { boss, party, events: [] }
|
||||
if (distanceVec2(boss.position, target.position) > RIMEBASTION_BALANCE.meleeRange + target.radius) {
|
||||
return { boss, party, events: [] }
|
||||
}
|
||||
const result = applyPartyDamageInShape(party, {
|
||||
kind: 'circle',
|
||||
position: boss.position,
|
||||
radius: RIMEBASTION_BALANCE.meleeRange,
|
||||
}, {
|
||||
damage: RIMEBASTION_BALANCE.meleeDamage,
|
||||
sourceId: boss.id,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
boss: { ...boss, meleeCooldownRemaining: RIMEBASTION_BALANCE.meleeCooldown },
|
||||
events: result.events,
|
||||
party: result.party,
|
||||
}
|
||||
}
|
||||
|
||||
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 partyCenter(party: Iwt2PartyEntityState[], fallback: Iwt2Vec2): Iwt2Vec2 {
|
||||
if (party.length <= 0) return fallback
|
||||
const total = party.reduce((sum, member) => addVec2(sum, member.position), { x: 0, y: 0 })
|
||||
return scaleVec2(total, 1 / party.length)
|
||||
}
|
||||
|
||||
function arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
return {
|
||||
x: state.bounds.width * 0.5,
|
||||
y: state.bounds.height * 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
function isBossNearWall(boss: Pick<Iwt2BossEntityState, 'position' | 'radius'>, state: Iwt2ArenaState): boolean {
|
||||
return isPositionNearWall(boss.position, boss.radius + RIMEBASTION_BALANCE.wallMargin, state)
|
||||
}
|
||||
|
||||
function isPositionNearWall(position: Iwt2Vec2, margin: number, state: Iwt2ArenaState): boolean {
|
||||
return (
|
||||
position.x <= margin
|
||||
|| position.x >= state.bounds.width - margin
|
||||
|| position.y <= margin
|
||||
|| position.y >= state.bounds.height - margin
|
||||
)
|
||||
}
|
||||
|
||||
function withRimebastionIndicators(result: {
|
||||
boss: RimebastionBossState
|
||||
party: Iwt2PartyEntityState[]
|
||||
events: Iwt2ArenaEvent[]
|
||||
state: Iwt2ArenaState
|
||||
}): Iwt2BossTickResult {
|
||||
const { state, ...rest } = result
|
||||
return {
|
||||
...rest,
|
||||
boss: result.boss as Iwt2BossEntityState,
|
||||
indicators: createRimebastionIndicators(result.boss, state),
|
||||
}
|
||||
}
|
||||
|
||||
function createRimebastionIndicators(boss: RimebastionBossState, state: Iwt2ArenaState): Iwt2ArenaIndicator[] {
|
||||
const indicators: Iwt2ArenaIndicator[] = []
|
||||
const showWalls = (
|
||||
boss.attackPhase === 'iceWallWindup'
|
||||
|| boss.attackPhase === 'shardShatterWindup'
|
||||
|| boss.attackPhase === 'shardShatterRecover'
|
||||
)
|
||||
if (showWalls) {
|
||||
const wallPhase: Iwt2ArenaIndicatorPhase = boss.attackPhase === 'iceWallWindup'
|
||||
? 'windup'
|
||||
: boss.attackPhase === 'shardShatterWindup'
|
||||
? 'active'
|
||||
: 'recover'
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
indicators.push(createLaneIndicator({
|
||||
color: WALL_COLOR,
|
||||
end: lane.end,
|
||||
id: `${boss.id}:${lane.id}`,
|
||||
mechanicId: 'rimebastion-ice-wall-lane',
|
||||
phase: wallPhase,
|
||||
sourceId: boss.id,
|
||||
start: lane.start,
|
||||
width: lane.width,
|
||||
}))
|
||||
}
|
||||
for (const circle of boss.mechanicCircles) {
|
||||
indicators.push(createCircleIndicator({
|
||||
color: WALL_COLOR,
|
||||
id: `${boss.id}:${circle.id}`,
|
||||
mechanicId: 'rimebastion-ice-wall-circle',
|
||||
phase: wallPhase,
|
||||
position: circle.position,
|
||||
radius: circle.radius,
|
||||
sourceId: boss.id,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'shardShatterWindup' || boss.attackPhase === 'shardShatterRecover') {
|
||||
const shardPhase = indicatorPhaseFromAttack(
|
||||
boss.attackPhase === 'shardShatterWindup',
|
||||
boss.attackPhase === 'shardShatterRecover',
|
||||
)
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
indicators.push(createLaneIndicator({
|
||||
color: SHARD_COLOR,
|
||||
end: lane.end,
|
||||
id: `${boss.id}:${lane.id}:shatter`,
|
||||
mechanicId: 'rimebastion-shard-lane',
|
||||
phase: shardPhase,
|
||||
sourceId: boss.id,
|
||||
start: lane.start,
|
||||
width: RIMEBASTION_BALANCE.shatterLaneWidth,
|
||||
}))
|
||||
}
|
||||
for (const cone of createShardCones(boss, state)) {
|
||||
indicators.push(createConeIndicator({
|
||||
angleRadians: cone.angleRadians,
|
||||
color: SHARD_COLOR,
|
||||
direction: cone.direction,
|
||||
id: `${boss.id}:${cone.id}`,
|
||||
mechanicId: 'rimebastion-shard-cone',
|
||||
origin: cone.origin,
|
||||
phase: shardPhase,
|
||||
range: cone.range,
|
||||
sourceId: boss.id,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return indicators
|
||||
}
|
||||
Reference in New Issue
Block a user