Android build v1.1.15
This commit is contained in:
@@ -0,0 +1,646 @@
|
||||
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
|
||||
import { IWT2_CLASS_METADATA } from '../content/classes'
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2ArenaInput,
|
||||
Iwt2ArenaState,
|
||||
Iwt2EntityId,
|
||||
Iwt2GroundHazardState,
|
||||
Iwt2HostileAddState,
|
||||
Iwt2PartyAiRole,
|
||||
Iwt2PartyEntityId,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2ProjectileEntityState,
|
||||
Iwt2StatusState,
|
||||
} from './types'
|
||||
import type { Iwt2PlayerClassId } from '../content/classes'
|
||||
import { tickBoss } from './bossAi'
|
||||
import { separateCircles } from './collision'
|
||||
import { canPartyMemberHitTarget, tickPartyMember } from './partyAi'
|
||||
import { addFirePuddle, createHazardIndicators, tickGroundHazards } from './hazards'
|
||||
import { applyPartyDamageInShape } from './mechanics'
|
||||
import {
|
||||
clampVec2ToArena,
|
||||
distanceVec2,
|
||||
normalizeVec2,
|
||||
scaleVec2,
|
||||
subtractVec2,
|
||||
} from './vector'
|
||||
|
||||
const DEFAULT_ARENA_WIDTH = 960
|
||||
const DEFAULT_ARENA_HEIGHT = 540
|
||||
const MAX_DT = 1 / 15
|
||||
const MAX_EVENTS = 80
|
||||
const HEALER_MANA_REGEN_PER_SECOND = 3
|
||||
const BOSS_PROJECTILE_BOUNCE_COOLDOWN_SECONDS = 0.22
|
||||
|
||||
type InitialPartyMember = {
|
||||
id: Iwt2PartyEntityId
|
||||
classId: Iwt2PlayerClassId
|
||||
aiRole: Iwt2PartyAiRole
|
||||
x: number
|
||||
y: number
|
||||
preferredOffset: { x: number; y: number }
|
||||
decisionOffset: number
|
||||
}
|
||||
|
||||
const INITIAL_PARTY: InitialPartyMember[] = [
|
||||
{ id: 'player-healer', classId: 'healer', aiRole: 'player', x: 250, y: 250, preferredOffset: { x: -260, y: 0 }, decisionOffset: 0 },
|
||||
{ id: 'paladin-tank', classId: 'paladin', aiRole: 'tank', x: 470, y: 250, preferredOffset: { x: -58, y: 6 }, decisionOffset: 0.08 },
|
||||
{ id: 'ranger', classId: 'ranger', aiRole: 'ranged', x: 280, y: 140, preferredOffset: { x: -230, y: -92 }, decisionOffset: 0.18 },
|
||||
{ id: 'mage', classId: 'mage', aiRole: 'ranged', x: 300, y: 365, preferredOffset: { x: -190, y: 98 }, decisionOffset: 0.32 },
|
||||
{ id: 'rogue', classId: 'rogue', aiRole: 'flanker', x: 520, y: 195, preferredOffset: { x: 24, y: -58 }, decisionOffset: 0.12 },
|
||||
{ id: 'warrior', classId: 'warrior', aiRole: 'melee', x: 515, y: 310, preferredOffset: { x: -18, y: 64 }, decisionOffset: 0.24 },
|
||||
]
|
||||
|
||||
export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome'): Iwt2ArenaState {
|
||||
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
time: 0,
|
||||
bounds: { width: DEFAULT_ARENA_WIDTH, height: DEFAULT_ARENA_HEIGHT },
|
||||
party: INITIAL_PARTY.map(createPartyMember),
|
||||
projectiles: [],
|
||||
hostileAdds: [],
|
||||
hazards: [],
|
||||
indicators: [],
|
||||
boss: {
|
||||
id: bossId,
|
||||
kind: 'boss',
|
||||
bossId,
|
||||
position: { x: 640, y: 250 },
|
||||
velocity: { x: 0, y: 0 },
|
||||
facing: { x: -1, y: 0 },
|
||||
radius: bossMetadata.radius,
|
||||
health: bossMetadata.maxHealth,
|
||||
maxHealth: bossMetadata.maxHealth,
|
||||
meleeCooldownRemaining: 0.6,
|
||||
chargeCooldownRemaining: bossId === 'bulldrome' ? 2 : 0,
|
||||
chargeCount: 0,
|
||||
attackPhase: 'idle',
|
||||
phaseSecondsRemaining: 0,
|
||||
chargeStart: { x: 640, y: 250 },
|
||||
chargeEnd: { x: 640, y: 250 },
|
||||
chargeHitEntityIds: [],
|
||||
slamApplied: false,
|
||||
wallContactSeconds: 0,
|
||||
relocateTarget: { x: DEFAULT_ARENA_WIDTH * 0.58, y: DEFAULT_ARENA_HEIGHT * 0.5 },
|
||||
fireballCooldownRemaining: bossId === 'yian-kut-ku' ? 1.2 : 0,
|
||||
fireballTarget: { x: 320, y: 250 },
|
||||
birdWaveThresholdsTriggered: [],
|
||||
},
|
||||
nextEventId: 1,
|
||||
nextProjectileId: 1,
|
||||
nextAddId: 1,
|
||||
nextHazardId: 1,
|
||||
events: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
|
||||
const step = Math.max(0, Math.min(dt, MAX_DT))
|
||||
if (step <= 0) return { ...state, events: [...state.events] }
|
||||
|
||||
const inputMove = {
|
||||
x: clampInputAxis(input.moveX),
|
||||
y: clampInputAxis(input.moveY),
|
||||
}
|
||||
const baseState = {
|
||||
...state,
|
||||
time: state.time + step,
|
||||
party: state.party.map((member) => tickPartyMember(member, state, inputMove, step)),
|
||||
events: [],
|
||||
}
|
||||
const separatedState = {
|
||||
...baseState,
|
||||
party: separatePartyFromBoss(baseState.party, baseState),
|
||||
}
|
||||
const bossResult = tickBoss(separatedState, step)
|
||||
const projectileResult = advanceProjectiles(
|
||||
[...separatedState.projectiles, ...(bossResult.projectiles ?? [])],
|
||||
bossResult.party,
|
||||
bossResult.boss,
|
||||
bossResult.hostileAdds ?? separatedState.hostileAdds,
|
||||
bossResult.hazards ?? separatedState.hazards,
|
||||
bossResult.nextHazardId ?? state.nextHazardId,
|
||||
bossResult.nextProjectileId ?? state.nextProjectileId,
|
||||
separatedState.bounds,
|
||||
separatedState.time,
|
||||
step,
|
||||
)
|
||||
const hazardResult = tickGroundHazards({
|
||||
dt: step,
|
||||
hazards: projectileResult.hazards,
|
||||
party: projectileResult.party,
|
||||
time: separatedState.time,
|
||||
})
|
||||
const damageResult = applyPartyAttacks(
|
||||
hazardResult.party,
|
||||
projectileResult.boss,
|
||||
projectileResult.hostileAdds,
|
||||
separatedState.time,
|
||||
projectileResult.nextProjectileId,
|
||||
)
|
||||
const hotResult = tickPartyHotEffects(damageResult.party, step, separatedState.time)
|
||||
const finalParty = regeneratePartyMana(hotResult.party, step)
|
||||
const nextEvents = assignEventIds(
|
||||
[...bossResult.events, ...projectileResult.events, ...hazardResult.events, ...damageResult.events, ...hotResult.events],
|
||||
state.nextEventId,
|
||||
)
|
||||
return {
|
||||
...separatedState,
|
||||
boss: damageResult.boss,
|
||||
indicators: [...bossResult.indicators, ...createHazardIndicators(hazardResult.hazards)],
|
||||
party: finalParty,
|
||||
hostileAdds: damageResult.hostileAdds,
|
||||
hazards: hazardResult.hazards,
|
||||
projectiles: [...projectileResult.projectiles, ...damageResult.projectiles],
|
||||
nextProjectileId: damageResult.nextProjectileId,
|
||||
nextAddId: bossResult.nextAddId ?? state.nextAddId,
|
||||
nextHazardId: projectileResult.nextHazardId,
|
||||
nextEventId: state.nextEventId + nextEvents.length,
|
||||
events: [...state.events, ...nextEvents].slice(-MAX_EVENTS),
|
||||
}
|
||||
}
|
||||
|
||||
function tickPartyHotEffects(
|
||||
party: Iwt2PartyEntityState[],
|
||||
dt: number,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const nextParty = party.map((member) => {
|
||||
if (member.hotEffects.length === 0) return member
|
||||
|
||||
let health = member.health
|
||||
const hotEffects = member.hotEffects.flatMap((effect) => {
|
||||
const remainingSeconds = effect.remainingSeconds - dt
|
||||
if (remainingSeconds <= 0) return []
|
||||
let nextTickInSeconds = effect.nextTickInSeconds - dt
|
||||
if (member.health > 0 && nextTickInSeconds <= 0) {
|
||||
const before = health
|
||||
health = Math.min(member.maxHealth, health + effect.power)
|
||||
const healed = health - before
|
||||
if (healed > 0) {
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'partyHealed',
|
||||
sourceId: 'player-healer',
|
||||
targetId: member.id,
|
||||
value: healed,
|
||||
})
|
||||
}
|
||||
nextTickInSeconds += effect.tickIntervalSeconds
|
||||
}
|
||||
return [{
|
||||
...effect,
|
||||
nextTickInSeconds,
|
||||
remainingSeconds,
|
||||
}]
|
||||
})
|
||||
|
||||
return {
|
||||
...member,
|
||||
health,
|
||||
hotEffects,
|
||||
}
|
||||
})
|
||||
return { party: nextParty, events }
|
||||
}
|
||||
|
||||
function regeneratePartyMana(party: Iwt2PartyEntityState[], dt: number): Iwt2PartyEntityState[] {
|
||||
return party.map((member) => {
|
||||
if (member.maxMana <= 0 || member.health <= 0 || member.mana >= member.maxMana) return member
|
||||
return {
|
||||
...member,
|
||||
mana: Math.min(member.maxMana, member.mana + HEALER_MANA_REGEN_PER_SECOND * dt),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createPartyMember(initial: InitialPartyMember): Iwt2PartyEntityState {
|
||||
const metadata = IWT2_CLASS_METADATA[initial.classId]
|
||||
return {
|
||||
id: initial.id,
|
||||
kind: 'party',
|
||||
classId: initial.classId,
|
||||
aiRole: initial.aiRole,
|
||||
position: { x: initial.x, y: initial.y },
|
||||
velocity: { x: 0, y: 0 },
|
||||
facing: { x: 1, y: 0 },
|
||||
radius: metadata.radius,
|
||||
health: metadata.maxHealth,
|
||||
maxHealth: metadata.maxHealth,
|
||||
shield: 0,
|
||||
mana: initial.classId === 'healer' ? 100 : 0,
|
||||
maxMana: initial.classId === 'healer' ? 100 : 0,
|
||||
damageDone: 0,
|
||||
attackCooldownRemaining: initial.classId === 'healer' ? 0 : metadata.attackCooldown * 0.35,
|
||||
castSecondsRemaining: 0,
|
||||
attackReady: false,
|
||||
status: createEmptyStatus(),
|
||||
hotEffects: [],
|
||||
preferredOffset: { ...initial.preferredOffset },
|
||||
decisionSecondsRemaining: initial.decisionOffset,
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyStatus(): Iwt2StatusState {
|
||||
return {
|
||||
stunnedSeconds: 0,
|
||||
knockedDownSeconds: 0,
|
||||
invulnerableSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function clampInputAxis(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return Math.min(1, Math.max(-1, value))
|
||||
}
|
||||
|
||||
function separatePartyFromBoss(party: Iwt2PartyEntityState[], state: Iwt2ArenaState): Iwt2PartyEntityState[] {
|
||||
return party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
const position = separateCircles(
|
||||
{ position: member.position, radius: member.radius },
|
||||
{ position: state.boss.position, radius: state.boss.radius },
|
||||
state.bounds,
|
||||
)
|
||||
return { ...member, position }
|
||||
})
|
||||
}
|
||||
|
||||
function advanceProjectiles(
|
||||
projectiles: Iwt2ProjectileEntityState[],
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2ArenaState['boss'],
|
||||
hostileAdds: Iwt2HostileAddState[],
|
||||
hazards: Iwt2GroundHazardState[],
|
||||
nextHazardId: number,
|
||||
nextProjectileId: number,
|
||||
bounds: Iwt2ArenaState['bounds'],
|
||||
time: number,
|
||||
dt: number,
|
||||
): {
|
||||
boss: Iwt2ArenaState['boss']
|
||||
party: Iwt2PartyEntityState[]
|
||||
hostileAdds: Iwt2HostileAddState[]
|
||||
hazards: Iwt2GroundHazardState[]
|
||||
nextHazardId: number
|
||||
nextProjectileId: number
|
||||
projectiles: Iwt2ProjectileEntityState[]
|
||||
events: Iwt2ArenaEvent[]
|
||||
} {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
let nextBoss = boss
|
||||
let nextParty = party
|
||||
let nextHostileAdds = hostileAdds
|
||||
let nextHazards = hazards
|
||||
let nextHazardIdValue = nextHazardId
|
||||
const nextProjectiles: Iwt2ProjectileEntityState[] = []
|
||||
|
||||
for (const projectile of projectiles) {
|
||||
if (projectile.owner === 'boss') {
|
||||
const result = advanceBossProjectile({
|
||||
bounds,
|
||||
dt,
|
||||
hazards: nextHazards,
|
||||
nextHazardId: nextHazardIdValue,
|
||||
party: nextParty,
|
||||
projectile,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
nextHazards = result.hazards
|
||||
nextHazardIdValue = result.nextHazardId
|
||||
events.push(...result.events)
|
||||
if (result.projectile) nextProjectiles.push(result.projectile)
|
||||
continue
|
||||
}
|
||||
|
||||
if (nextBoss.health <= 0 && nextHostileAdds.every((add) => add.health <= 0)) continue
|
||||
const nextPosition = {
|
||||
x: projectile.position.x + projectile.velocity.x * dt,
|
||||
y: projectile.position.y + projectile.velocity.y * dt,
|
||||
}
|
||||
const remainingSeconds = projectile.remainingSeconds - dt
|
||||
const hitAdd = nextHostileAdds.find((add) => (
|
||||
add.health > 0
|
||||
&& distanceVec2(nextPosition, add.position) <= add.radius + projectile.radius
|
||||
))
|
||||
if (remainingSeconds <= 0) continue
|
||||
if (hitAdd) {
|
||||
const damage = Math.min(projectile.damage, hitAdd.health)
|
||||
nextHostileAdds = nextHostileAdds.map((add) => add.id === hitAdd.id
|
||||
? { ...add, health: Math.max(0, add.health - damage) }
|
||||
: add)
|
||||
nextParty = addDamageDone(nextParty, projectile.sourceId, damage)
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'bossDamaged',
|
||||
sourceId: projectile.sourceId,
|
||||
targetId: hitAdd.id,
|
||||
value: damage,
|
||||
})
|
||||
if (hitAdd.health - damage <= 0) {
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'entityDefeated',
|
||||
sourceId: projectile.sourceId,
|
||||
targetId: hitAdd.id,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (distanceVec2(nextPosition, nextBoss.position) <= nextBoss.radius + projectile.radius) {
|
||||
const damage = Math.min(projectile.damage, nextBoss.health)
|
||||
nextBoss = {
|
||||
...nextBoss,
|
||||
health: Math.max(0, nextBoss.health - damage),
|
||||
}
|
||||
nextParty = addDamageDone(nextParty, projectile.sourceId, damage)
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'bossDamaged',
|
||||
sourceId: projectile.sourceId,
|
||||
targetId: nextBoss.id,
|
||||
value: damage,
|
||||
})
|
||||
if (nextBoss.health <= 0) {
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'entityDefeated',
|
||||
sourceId: projectile.sourceId,
|
||||
targetId: nextBoss.id,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
nextProjectiles.push({
|
||||
...projectile,
|
||||
position: nextPosition,
|
||||
remainingSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
boss: nextBoss,
|
||||
party: nextParty,
|
||||
hostileAdds: nextHostileAdds,
|
||||
hazards: nextHazards,
|
||||
nextHazardId: nextHazardIdValue,
|
||||
nextProjectileId,
|
||||
projectiles: nextProjectiles,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
function advanceBossProjectile({
|
||||
bounds,
|
||||
dt,
|
||||
hazards,
|
||||
nextHazardId,
|
||||
party,
|
||||
projectile,
|
||||
time,
|
||||
}: {
|
||||
bounds: Iwt2ArenaState['bounds']
|
||||
dt: number
|
||||
hazards: Iwt2GroundHazardState[]
|
||||
nextHazardId: number
|
||||
party: Iwt2PartyEntityState[]
|
||||
projectile: Iwt2ProjectileEntityState
|
||||
time: number
|
||||
}): {
|
||||
party: Iwt2PartyEntityState[]
|
||||
hazards: Iwt2GroundHazardState[]
|
||||
nextHazardId: number
|
||||
projectile?: Iwt2ProjectileEntityState
|
||||
events: Iwt2ArenaEvent[]
|
||||
} {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const remainingSeconds = projectile.remainingSeconds - dt
|
||||
if (remainingSeconds <= 0) return { party, hazards, nextHazardId, events }
|
||||
|
||||
let nextParty = party
|
||||
let nextHazards = hazards
|
||||
let nextHazardIdValue = nextHazardId
|
||||
let bounced = false
|
||||
const bounceCooldownSeconds = Math.max(0, (projectile.bounceCooldownSeconds ?? 0) - dt)
|
||||
const canBounce = bounceCooldownSeconds <= 0
|
||||
let velocity = { ...projectile.velocity }
|
||||
let position = {
|
||||
x: projectile.position.x + projectile.velocity.x * dt,
|
||||
y: projectile.position.y + projectile.velocity.y * dt,
|
||||
}
|
||||
|
||||
if (canBounce && (position.x - projectile.radius <= 0 || position.x + projectile.radius >= bounds.width)) {
|
||||
velocity = { ...velocity, x: -velocity.x }
|
||||
position = clampVec2ToArena(position, projectile.radius, bounds)
|
||||
bounced = true
|
||||
}
|
||||
if (canBounce && (position.y - projectile.radius <= 0 || position.y + projectile.radius >= bounds.height)) {
|
||||
velocity = { ...velocity, y: -velocity.y }
|
||||
position = clampVec2ToArena(position, projectile.radius, bounds)
|
||||
bounced = true
|
||||
}
|
||||
|
||||
const hitMember = canBounce
|
||||
? nextParty.find((member) => (
|
||||
member.health > 0
|
||||
&& distanceVec2(position, member.position) <= projectile.radius + member.radius
|
||||
))
|
||||
: undefined
|
||||
if (hitMember) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
kind: 'circle',
|
||||
position: hitMember.position,
|
||||
radius: hitMember.radius + projectile.radius,
|
||||
}, {
|
||||
damage: projectile.damage,
|
||||
sourceId: projectile.sourceId,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
events.push(...result.events)
|
||||
const away = normalizeVec2(subtractVec2(position, hitMember.position))
|
||||
const fallback = normalizeVec2(scaleVec2(projectile.velocity, -1))
|
||||
const direction = away.x === 0 && away.y === 0 ? fallback : away
|
||||
const speed = Math.max(1, Math.hypot(projectile.velocity.x, projectile.velocity.y))
|
||||
velocity = scaleVec2(direction, speed)
|
||||
position = clampVec2ToArena({
|
||||
x: hitMember.position.x + direction.x * (hitMember.radius + projectile.radius + 2),
|
||||
y: hitMember.position.y + direction.y * (hitMember.radius + projectile.radius + 2),
|
||||
}, projectile.radius, bounds)
|
||||
bounced = true
|
||||
}
|
||||
|
||||
if (bounced) {
|
||||
const puddle = addFirePuddle({
|
||||
damage: IWT2_BOSS_METADATA['yian-kut-ku'].firePuddleDamage!,
|
||||
duration: IWT2_BOSS_METADATA['yian-kut-ku'].firePuddleSeconds!,
|
||||
hazards: nextHazards,
|
||||
nextHazardId: nextHazardIdValue,
|
||||
position: hitMember?.position ?? position,
|
||||
radius: IWT2_BOSS_METADATA['yian-kut-ku'].firePuddleRadius!,
|
||||
sourceId: projectile.sourceId,
|
||||
time,
|
||||
})
|
||||
nextHazards = puddle.hazards
|
||||
nextHazardIdValue = puddle.nextHazardId
|
||||
}
|
||||
|
||||
const bouncesRemaining = bounced
|
||||
? (projectile.bouncesRemaining ?? 0) - 1
|
||||
: projectile.bouncesRemaining
|
||||
if (bouncesRemaining !== undefined && bouncesRemaining < 0) {
|
||||
return { party: nextParty, hazards: nextHazards, nextHazardId: nextHazardIdValue, events }
|
||||
}
|
||||
|
||||
return {
|
||||
events,
|
||||
hazards: nextHazards,
|
||||
nextHazardId: nextHazardIdValue,
|
||||
party: nextParty,
|
||||
projectile: {
|
||||
...projectile,
|
||||
bouncesRemaining,
|
||||
bounceCooldownSeconds: bounced ? BOSS_PROJECTILE_BOUNCE_COOLDOWN_SECONDS : bounceCooldownSeconds,
|
||||
position,
|
||||
remainingSeconds,
|
||||
velocity,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function applyPartyAttacks(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2ArenaState['boss'],
|
||||
hostileAdds: Iwt2HostileAddState[],
|
||||
time: number,
|
||||
nextProjectileId: number,
|
||||
): {
|
||||
boss: Iwt2ArenaState['boss']
|
||||
party: Iwt2PartyEntityState[]
|
||||
hostileAdds: Iwt2HostileAddState[]
|
||||
projectiles: Iwt2ProjectileEntityState[]
|
||||
nextProjectileId: number
|
||||
events: Iwt2ArenaEvent[]
|
||||
} {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const projectiles: Iwt2ProjectileEntityState[] = []
|
||||
let nextBoss = boss
|
||||
let nextHostileAdds = hostileAdds
|
||||
let projectileId = nextProjectileId
|
||||
const nextParty = party.map((member) => {
|
||||
const target = getPriorityAttackTarget(member, nextBoss, nextHostileAdds)
|
||||
if (!target) return member
|
||||
if (!canPartyMemberHitTarget(member, target.position)) return member
|
||||
const metadata = IWT2_CLASS_METADATA[member.classId]
|
||||
if (metadata.projectileSpeed > 0) {
|
||||
if (!member.attackReady) return member
|
||||
const direction = normalizeVec2(subtractVec2(target.position, member.position))
|
||||
projectiles.push({
|
||||
id: `projectile-${projectileId}`,
|
||||
sourceId: member.id,
|
||||
owner: 'party',
|
||||
classId: member.classId,
|
||||
projectileKind: member.classId === 'mage' ? 'magic' : 'arrow',
|
||||
color: metadata.accentColor,
|
||||
position: {
|
||||
x: member.position.x + direction.x * (member.radius + 4),
|
||||
y: member.position.y + direction.y * (member.radius + 4),
|
||||
},
|
||||
velocity: scaleVec2(direction, metadata.projectileSpeed),
|
||||
radius: member.classId === 'mage' ? 7 : 4,
|
||||
damage: metadata.attackDamage,
|
||||
remainingSeconds: 1.2,
|
||||
})
|
||||
projectileId += 1
|
||||
return {
|
||||
...member,
|
||||
attackCooldownRemaining: metadata.attackCooldown,
|
||||
attackReady: false,
|
||||
}
|
||||
}
|
||||
if (member.attackCooldownRemaining > 0) return member
|
||||
const damage = Math.min(metadata.attackDamage, target.health)
|
||||
if (target.kind === 'boss') {
|
||||
nextBoss = { ...nextBoss, health: Math.max(0, nextBoss.health - damage) }
|
||||
} else {
|
||||
nextHostileAdds = nextHostileAdds.map((add) => add.id === target.id
|
||||
? { ...add, health: Math.max(0, add.health - damage) }
|
||||
: add)
|
||||
}
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'bossDamaged',
|
||||
sourceId: member.id,
|
||||
targetId: target.id,
|
||||
value: damage,
|
||||
})
|
||||
if (target.health - damage <= 0) {
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
type: 'entityDefeated',
|
||||
sourceId: member.id,
|
||||
targetId: target.id,
|
||||
})
|
||||
return {
|
||||
...member,
|
||||
damageDone: member.damageDone + damage,
|
||||
attackCooldownRemaining: metadata.attackCooldown,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...member,
|
||||
damageDone: member.damageDone + damage,
|
||||
attackCooldownRemaining: metadata.attackCooldown,
|
||||
}
|
||||
})
|
||||
return {
|
||||
boss: nextBoss,
|
||||
party: nextParty,
|
||||
hostileAdds: nextHostileAdds.filter((add) => add.health > 0),
|
||||
projectiles,
|
||||
nextProjectileId: projectileId,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
function getPriorityAttackTarget(
|
||||
member: Iwt2PartyEntityState,
|
||||
boss: Iwt2ArenaState['boss'],
|
||||
hostileAdds: Iwt2HostileAddState[],
|
||||
): (Iwt2ArenaState['boss'] | Iwt2HostileAddState) | undefined {
|
||||
if (member.health <= 0) return undefined
|
||||
const livingAdds = hostileAdds.filter((add) => add.health > 0)
|
||||
if (livingAdds.length > 0) {
|
||||
return livingAdds.reduce((best, add) => (
|
||||
distanceVec2(member.position, add.position) < distanceVec2(member.position, best.position) ? add : best
|
||||
), livingAdds[0])
|
||||
}
|
||||
return boss.health > 0 ? boss : undefined
|
||||
}
|
||||
|
||||
function addDamageDone(
|
||||
party: Iwt2PartyEntityState[],
|
||||
sourceId: Iwt2EntityId,
|
||||
damage: number,
|
||||
): Iwt2PartyEntityState[] {
|
||||
return party.map((member) => member.id === sourceId
|
||||
? { ...member, damageDone: member.damageDone + damage }
|
||||
: member)
|
||||
}
|
||||
|
||||
function assignEventIds(events: Iwt2ArenaEvent[], firstId: number): Iwt2ArenaEvent[] {
|
||||
return events.map((event, index) => ({ ...event, id: firstId + index }))
|
||||
}
|
||||
Reference in New Issue
Block a user