Android build v1.1.15
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
import { BULLDROME_BOSS_METADATA } from '../content/bosses'
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2ArenaIndicator,
|
||||
Iwt2ArenaState,
|
||||
Iwt2BossEntityState,
|
||||
Iwt2EntityId,
|
||||
Iwt2GroundHazardState,
|
||||
Iwt2HostileAddState,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2ProjectileEntityState,
|
||||
Iwt2Vec2,
|
||||
} from './types'
|
||||
import { tickYianKutKu } from './yianKutKuAi'
|
||||
import {
|
||||
applyPartyDamageInShape,
|
||||
createArenaEvent,
|
||||
createCircleIndicator,
|
||||
createLaneIndicator,
|
||||
createLinearMovementPlan,
|
||||
indicatorPhaseFromAttack,
|
||||
} from './mechanics'
|
||||
import {
|
||||
clampVec2ToArena,
|
||||
distanceVec2,
|
||||
moveToward,
|
||||
scaleVec2,
|
||||
subtractVec2,
|
||||
withFallbackFacing,
|
||||
} from './vector'
|
||||
|
||||
export type Iwt2BossTickResult = {
|
||||
boss: Iwt2BossEntityState
|
||||
party: Iwt2PartyEntityState[]
|
||||
hostileAdds?: Iwt2HostileAddState[]
|
||||
hazards?: Iwt2GroundHazardState[]
|
||||
projectiles?: Iwt2ProjectileEntityState[]
|
||||
events: Iwt2ArenaEvent[]
|
||||
indicators: Iwt2ArenaIndicator[]
|
||||
nextAddId?: number
|
||||
nextHazardId?: number
|
||||
nextProjectileId?: number
|
||||
}
|
||||
|
||||
export function tickBoss(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult {
|
||||
if (state.boss.bossId === 'yian-kut-ku') return tickYianKutKu(state, dt)
|
||||
return tickBulldrome(state, dt)
|
||||
}
|
||||
|
||||
export function tickBulldrome(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const target = getBossTarget(state.party)
|
||||
let party = state.party
|
||||
let boss = {
|
||||
...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),
|
||||
}
|
||||
const wallContactSeconds = isBossNearWall(boss, state)
|
||||
? boss.wallContactSeconds + dt
|
||||
: Math.max(0, boss.wallContactSeconds - dt * 2)
|
||||
boss = { ...boss, wallContactSeconds }
|
||||
|
||||
if (boss.health <= 0 || !target) {
|
||||
return withBulldromeIndicators({ boss: { ...boss, velocity: { x: 0, y: 0 } }, party, events })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'relocating') {
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, boss.relocateTarget, BULLDROME_BOSS_METADATA.moveSpeed * 1.45 * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
const velocity = scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0)
|
||||
boss = {
|
||||
...boss,
|
||||
position: nextPosition,
|
||||
velocity,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
wallContactSeconds: isBossNearWall({ ...boss, position: nextPosition }, state) ? boss.wallContactSeconds : 0,
|
||||
}
|
||||
if (boss.phaseSecondsRemaining <= 0 || distanceVec2(nextPosition, boss.relocateTarget) <= 8) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
phaseSecondsRemaining: 0,
|
||||
chargeCooldownRemaining: Math.max(boss.chargeCooldownRemaining, 1.2),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'chargeWindup') {
|
||||
boss = {
|
||||
...boss,
|
||||
velocity: { x: 0, y: 0 },
|
||||
facing: withFallbackFacing(subtractVec2(boss.chargeEnd, boss.position), boss.facing),
|
||||
}
|
||||
if (boss.phaseSecondsRemaining <= 0) boss = { ...boss, attackPhase: 'charging' }
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'charging') {
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, boss.chargeEnd, BULLDROME_BOSS_METADATA.chargeSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
const hitResult = applyChargeHits(party, boss, boss.position, nextPosition, state.time + dt)
|
||||
party = hitResult.party
|
||||
events.push(...hitResult.events)
|
||||
const velocity = scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0)
|
||||
boss = {
|
||||
...boss,
|
||||
position: nextPosition,
|
||||
velocity,
|
||||
facing: withFallbackFacing(velocity, boss.facing),
|
||||
chargeHitEntityIds: hitResult.hitEntityIds,
|
||||
}
|
||||
if (distanceVec2(nextPosition, boss.chargeEnd) <= 1) {
|
||||
const needsSlam = boss.chargeCount % 3 === 0
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: needsSlam ? 'slamWindup' : 'chargeRecover',
|
||||
phaseSecondsRemaining: needsSlam ? BULLDROME_BOSS_METADATA.slamWindup : 0.45,
|
||||
slamApplied: false,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'slamWindup') {
|
||||
boss = { ...boss, velocity: { x: 0, y: 0 } }
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const slamResult = applySlam(party, boss, state.time + dt)
|
||||
party = slamResult.party
|
||||
events.push(...slamResult.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'slamRecover',
|
||||
phaseSecondsRemaining: 0.5,
|
||||
slamApplied: true,
|
||||
}
|
||||
}
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'chargeRecover' || boss.attackPhase === 'slamRecover') {
|
||||
boss = { ...boss, velocity: { x: 0, y: 0 } }
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = { ...boss, attackPhase: 'idle', phaseSecondsRemaining: 0 }
|
||||
}
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
if (boss.attackPhase === 'idle' && boss.wallContactSeconds >= 1.1) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'relocating',
|
||||
phaseSecondsRemaining: 1.8,
|
||||
relocateTarget: relocationTarget(boss, state),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
if (boss.chargeCooldownRemaining <= 0) {
|
||||
const charge = createLinearMovementPlan({
|
||||
bounds: state.bounds,
|
||||
from: boss.position,
|
||||
length: BULLDROME_BOSS_METADATA.chargeLength,
|
||||
radius: boss.radius,
|
||||
target: target.position,
|
||||
})
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'chargeWindup',
|
||||
phaseSecondsRemaining: BULLDROME_BOSS_METADATA.chargeWindup,
|
||||
chargeCooldownRemaining: BULLDROME_BOSS_METADATA.chargeCooldown,
|
||||
chargeCount: boss.chargeCount + 1,
|
||||
chargeStart: { ...boss.position },
|
||||
chargeEnd: charge.end,
|
||||
chargeHitEntityIds: [],
|
||||
slamApplied: false,
|
||||
facing: charge.direction,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
events.push(createArenaEvent(state.nextEventId + events.length, state.time + dt, 'bossChargeStart', boss.id, target.id))
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
const meleeResult = maybeApplyMelee(party, boss, target, state.time + dt, state.nextEventId + events.length)
|
||||
party = meleeResult.party
|
||||
events.push(...meleeResult.events)
|
||||
boss = meleeResult.boss
|
||||
|
||||
if (meleeResult.events.length > 0) return withBulldromeIndicators({ boss, party, events })
|
||||
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, target.position, BULLDROME_BOSS_METADATA.moveSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
const velocity = scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0)
|
||||
boss = {
|
||||
...boss,
|
||||
position: nextPosition,
|
||||
velocity,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
}
|
||||
return withBulldromeIndicators({ boss, party, events })
|
||||
}
|
||||
|
||||
function isBossNearWall(boss: Iwt2BossEntityState, state: Iwt2ArenaState) {
|
||||
const margin = boss.radius + 10
|
||||
return (
|
||||
boss.position.x <= margin
|
||||
|| boss.position.x >= state.bounds.width - margin
|
||||
|| boss.position.y <= margin
|
||||
|| boss.position.y >= state.bounds.height - margin
|
||||
)
|
||||
}
|
||||
|
||||
function relocationTarget(boss: Iwt2BossEntityState, state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
const center = { x: state.bounds.width * 0.58, y: state.bounds.height * 0.5 }
|
||||
const awayFromWall = {
|
||||
x: boss.position.x < state.bounds.width * 0.5 ? 1 : -1,
|
||||
y: boss.position.y < state.bounds.height * 0.5 ? 1 : -1,
|
||||
}
|
||||
return {
|
||||
x: Math.min(state.bounds.width - boss.radius, Math.max(boss.radius, center.x + awayFromWall.x * 70)),
|
||||
y: Math.min(state.bounds.height - boss.radius, Math.max(boss.radius, center.y + awayFromWall.y * 54)),
|
||||
}
|
||||
}
|
||||
|
||||
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 maybeApplyMelee(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
target: Iwt2PartyEntityState,
|
||||
time: number,
|
||||
firstEventId: number,
|
||||
): Omit<Iwt2BossTickResult, 'indicators'> {
|
||||
if (boss.meleeCooldownRemaining > 0) return { boss, party, events: [] }
|
||||
if (distanceVec2(boss.position, target.position) > BULLDROME_BOSS_METADATA.meleeRange + target.radius) {
|
||||
return { boss, party, events: [] }
|
||||
}
|
||||
const nextParty = party.map((member) => {
|
||||
if (member.id !== target.id) return member
|
||||
return { ...member, health: Math.max(0, member.health - BULLDROME_BOSS_METADATA.meleeDamage) }
|
||||
})
|
||||
const events = [
|
||||
createArenaEvent(firstEventId, time, 'partyDamaged', boss.id, target.id, BULLDROME_BOSS_METADATA.meleeDamage),
|
||||
]
|
||||
if (target.health > 0 && target.health - BULLDROME_BOSS_METADATA.meleeDamage <= 0) {
|
||||
events.push(createArenaEvent(firstEventId + 1, time, 'entityDefeated', boss.id, target.id))
|
||||
}
|
||||
return {
|
||||
boss: { ...boss, meleeCooldownRemaining: BULLDROME_BOSS_METADATA.meleeCooldown, velocity: { x: 0, y: 0 } },
|
||||
party: nextParty,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
function applyChargeHits(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
start: Iwt2Vec2,
|
||||
end: Iwt2Vec2,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], hitEntityIds: Iwt2EntityId[], events: Iwt2ArenaEvent[] } {
|
||||
const result = applyPartyDamageInShape(
|
||||
party,
|
||||
{
|
||||
kind: 'lane',
|
||||
start,
|
||||
end,
|
||||
width: boss.radius,
|
||||
},
|
||||
{
|
||||
damage: BULLDROME_BOSS_METADATA.chargeDamage,
|
||||
damageEventType: 'bossChargeHit',
|
||||
excludedEntityIds: boss.chargeHitEntityIds,
|
||||
knockdownSeconds: BULLDROME_BOSS_METADATA.chargeStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: BULLDROME_BOSS_METADATA.chargeStunSeconds,
|
||||
time,
|
||||
},
|
||||
)
|
||||
return {
|
||||
...result,
|
||||
hitEntityIds: [...boss.chargeHitEntityIds, ...result.hitEntityIds],
|
||||
}
|
||||
}
|
||||
|
||||
function applySlam(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
const result = applyPartyDamageInShape(
|
||||
party,
|
||||
{
|
||||
kind: 'circle',
|
||||
position: boss.position,
|
||||
radius: BULLDROME_BOSS_METADATA.slamRadius,
|
||||
},
|
||||
{
|
||||
damage: BULLDROME_BOSS_METADATA.slamDamage,
|
||||
knockdownSeconds: BULLDROME_BOSS_METADATA.slamStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: BULLDROME_BOSS_METADATA.slamStunSeconds,
|
||||
time,
|
||||
},
|
||||
)
|
||||
return {
|
||||
party: result.party,
|
||||
events: [
|
||||
createArenaEvent(0, time, 'bossSlam', boss.id, undefined, BULLDROME_BOSS_METADATA.slamRadius),
|
||||
...result.events,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function withBulldromeIndicators(
|
||||
result: Omit<Iwt2BossTickResult, 'indicators'>,
|
||||
): Iwt2BossTickResult {
|
||||
return {
|
||||
...result,
|
||||
indicators: createBulldromeIndicators(result.boss),
|
||||
}
|
||||
}
|
||||
|
||||
function createBulldromeIndicators(boss: Iwt2BossEntityState): Iwt2ArenaIndicator[] {
|
||||
const indicators: Iwt2ArenaIndicator[] = []
|
||||
if (
|
||||
boss.attackPhase === 'chargeWindup'
|
||||
|| boss.attackPhase === 'charging'
|
||||
|| boss.attackPhase === 'chargeRecover'
|
||||
) {
|
||||
indicators.push(createLaneIndicator({
|
||||
color: boss.attackPhase === 'charging' ? '#f05b4f' : '#f1c663',
|
||||
end: boss.chargeEnd,
|
||||
id: `${boss.id}:charge-lane`,
|
||||
mechanicId: 'bulldrome-charge',
|
||||
phase: indicatorPhaseFromAttack(
|
||||
boss.attackPhase === 'chargeWindup',
|
||||
boss.attackPhase === 'charging',
|
||||
),
|
||||
sourceId: boss.id,
|
||||
start: boss.chargeStart,
|
||||
width: boss.radius,
|
||||
}))
|
||||
}
|
||||
if (boss.attackPhase === 'slamWindup' || boss.attackPhase === 'slamRecover') {
|
||||
indicators.push(createCircleIndicator({
|
||||
color: '#ff7f50',
|
||||
id: `${boss.id}:slam-circle`,
|
||||
mechanicId: 'bulldrome-slam',
|
||||
phase: indicatorPhaseFromAttack(
|
||||
boss.attackPhase === 'slamWindup',
|
||||
boss.attackPhase === 'slamRecover',
|
||||
),
|
||||
position: boss.position,
|
||||
radius: BULLDROME_BOSS_METADATA.slamRadius,
|
||||
sourceId: boss.id,
|
||||
}))
|
||||
}
|
||||
return indicators
|
||||
}
|
||||
Reference in New Issue
Block a user