Android build v1.1.23
This commit is contained in:
+41
-16
@@ -13,6 +13,7 @@ import type {
|
||||
Iwt2PartyEntityId,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2ProjectileEntityState,
|
||||
Iwt2RoguelikePressureState,
|
||||
Iwt2StatusState,
|
||||
Iwt2Vec2,
|
||||
} from './types'
|
||||
@@ -22,6 +23,7 @@ import { separateCircles } from './collision'
|
||||
import { canPartyMemberHitTarget, tickPartyMember } from './partyAi'
|
||||
import { addFirePuddle, createHazardIndicators, tickGroundHazards } from './hazards'
|
||||
import { applyPartyDamageInShape } from './mechanics'
|
||||
import { tickRoguelikePressure } from './roguelikePressure'
|
||||
import {
|
||||
addVec2,
|
||||
clampVec2ToArena,
|
||||
@@ -62,6 +64,8 @@ export function createInitialIwt2ArenaState(
|
||||
bossId: Iwt2BossId = 'bulldrome',
|
||||
bossIds?: Iwt2BossId[],
|
||||
bossHealthScale = 1,
|
||||
partyDamageTakenScale = 1,
|
||||
roguelikePressure?: Iwt2RoguelikePressureState,
|
||||
): Iwt2ArenaState {
|
||||
const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId)
|
||||
const bosses = initialBossIds.map((id, index) => createBossEntity(id, index, bossHealthScale))
|
||||
@@ -69,11 +73,12 @@ export function createInitialIwt2ArenaState(
|
||||
schemaVersion: 1,
|
||||
time: 0,
|
||||
bounds: { width: DEFAULT_ARENA_WIDTH, height: DEFAULT_ARENA_HEIGHT },
|
||||
party: INITIAL_PARTY.map(createPartyMember),
|
||||
party: INITIAL_PARTY.map((member) => createPartyMember(member, partyDamageTakenScale)),
|
||||
projectiles: [],
|
||||
hostileAdds: [],
|
||||
hazards: [],
|
||||
indicators: [],
|
||||
roguelikePressure,
|
||||
boss: bosses[0],
|
||||
bosses,
|
||||
nextEventId: 1,
|
||||
@@ -216,8 +221,13 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
|
||||
party: projectileResult.party,
|
||||
time: separatedState.time,
|
||||
})
|
||||
const pressureResult = tickRoguelikePressure({
|
||||
party: hazardResult.party,
|
||||
pressure: separatedState.roguelikePressure,
|
||||
time: separatedState.time,
|
||||
})
|
||||
const damageResult = applyPartyAttacks(
|
||||
hazardResult.party,
|
||||
pressureResult.party,
|
||||
projectileResult.bosses,
|
||||
projectileResult.hostileAdds,
|
||||
separatedState.time,
|
||||
@@ -234,7 +244,7 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
|
||||
finalCollisionState,
|
||||
)
|
||||
const nextEvents = assignEventIds(
|
||||
[...bossResult.events, ...projectileResult.events, ...hazardResult.events, ...damageResult.events, ...hotResult.events],
|
||||
[...bossResult.events, ...projectileResult.events, ...hazardResult.events, ...pressureResult.events, ...damageResult.events, ...hotResult.events],
|
||||
state.nextEventId,
|
||||
)
|
||||
return {
|
||||
@@ -242,6 +252,7 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
|
||||
boss: getPrimaryBoss(damageResult.bosses),
|
||||
bosses: damageResult.bosses,
|
||||
indicators: [...bossResult.indicators, ...createHazardIndicators(hazardResult.hazards)],
|
||||
roguelikePressure: pressureResult.pressure,
|
||||
party: finalParty,
|
||||
hostileAdds: damageResult.hostileAdds,
|
||||
hazards: hazardResult.hazards,
|
||||
@@ -270,7 +281,10 @@ function tickPartyHotEffects(
|
||||
let nextTickInSeconds = effect.nextTickInSeconds - dt
|
||||
if (member.health > 0 && nextTickInSeconds <= 0) {
|
||||
const before = health
|
||||
health = Math.min(member.maxHealth, health + effect.power)
|
||||
const healingMultiplier = member.shield > 0
|
||||
? member.shieldedHotHealingMultiplier ?? 1
|
||||
: 1
|
||||
health = Math.min(member.maxHealth, health + effect.power * healingMultiplier)
|
||||
const healed = health - before
|
||||
if (healed > 0) {
|
||||
events.push({
|
||||
@@ -310,8 +324,9 @@ function regeneratePartyMana(party: Iwt2PartyEntityState[], dt: number): Iwt2Par
|
||||
})
|
||||
}
|
||||
|
||||
function createPartyMember(initial: InitialPartyMember): Iwt2PartyEntityState {
|
||||
function createPartyMember(initial: InitialPartyMember, damageTakenScale: number): Iwt2PartyEntityState {
|
||||
const metadata = IWT2_CLASS_METADATA[initial.classId]
|
||||
const safeDamageTakenScale = Number.isFinite(damageTakenScale) ? Math.max(0.01, damageTakenScale) : 1
|
||||
return {
|
||||
id: initial.id,
|
||||
kind: 'party',
|
||||
@@ -321,8 +336,16 @@ function createPartyMember(initial: InitialPartyMember): Iwt2PartyEntityState {
|
||||
velocity: { x: 0, y: 0 },
|
||||
facing: { x: 1, y: 0 },
|
||||
radius: metadata.radius,
|
||||
moveSpeed: metadata.moveSpeed,
|
||||
attackRange: metadata.attackRange,
|
||||
attackDamage: metadata.attackDamage,
|
||||
attackCooldown: metadata.attackCooldown,
|
||||
castTime: metadata.castTime,
|
||||
projectileSpeed: metadata.projectileSpeed,
|
||||
health: metadata.maxHealth,
|
||||
maxHealth: metadata.maxHealth,
|
||||
damageTakenScale: safeDamageTakenScale,
|
||||
stunTakenScale: 1,
|
||||
shield: 0,
|
||||
mana: initial.classId === 'healer' ? 100 : 0,
|
||||
maxMana: initial.classId === 'healer' ? 100 : 0,
|
||||
@@ -447,13 +470,15 @@ function separatePartyFromBosses(party: Iwt2PartyEntityState[], state: Iwt2Arena
|
||||
let position = member.position
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.health <= 0) continue
|
||||
const overlapBeforeSeparation = circleOverlapAmount({ position, radius: member.radius }, boss)
|
||||
const separated = separateCircles(
|
||||
{ position, radius: member.radius },
|
||||
{ position: boss.position, radius: boss.radius },
|
||||
state.bounds,
|
||||
)
|
||||
position = wallClearance(separated, member, state) <= 2
|
||||
|| circleOverlapAmount({ position: separated, radius: member.radius }, boss) > 0.25
|
||||
const overlapAfterSeparation = circleOverlapAmount({ position: separated, radius: member.radius }, boss)
|
||||
position = overlapBeforeSeparation > 0.25
|
||||
&& (wallClearance(separated, member, state) <= 2 || overlapAfterSeparation > 0.25)
|
||||
? separateFromBossTowardCenter(member, boss, state)
|
||||
: separated
|
||||
}
|
||||
@@ -802,7 +827,7 @@ function applyPartyAttacks(
|
||||
if (!target) return member
|
||||
if (!canPartyMemberHitTarget(member, target.position, target.radius)) return member
|
||||
const metadata = IWT2_CLASS_METADATA[member.classId]
|
||||
if (metadata.projectileSpeed > 0) {
|
||||
if (member.projectileSpeed > 0) {
|
||||
if (!member.attackReady) return member
|
||||
const direction = normalizeVec2(subtractVec2(target.position, member.position))
|
||||
projectiles.push({
|
||||
@@ -810,29 +835,29 @@ function applyPartyAttacks(
|
||||
sourceId: member.id,
|
||||
owner: 'party',
|
||||
classId: member.classId,
|
||||
projectileKind: member.classId === 'mage' ? 'magic' : 'arrow',
|
||||
projectileKind: member.classId === 'mage' ? 'fireball' : '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),
|
||||
velocity: scaleVec2(direction, member.projectileSpeed),
|
||||
radius: member.classId === 'mage' ? 7 : 4,
|
||||
damage: metadata.attackDamage,
|
||||
damage: member.attackDamage,
|
||||
remainingSeconds: 1.2,
|
||||
})
|
||||
projectileId += 1
|
||||
return {
|
||||
...member,
|
||||
attackCooldownRemaining: metadata.attackCooldown,
|
||||
attackCooldownRemaining: member.attackCooldown,
|
||||
attackReady: false,
|
||||
}
|
||||
}
|
||||
if (member.attackCooldownRemaining > 0) return member
|
||||
let damage = Math.min(metadata.attackDamage, target.health)
|
||||
let damage = Math.min(member.attackDamage, target.health)
|
||||
let defeated = target.health - damage <= 0
|
||||
if (target.kind === 'boss') {
|
||||
const damageResult = applyDamageToBoss(target, metadata.attackDamage)
|
||||
const damageResult = applyDamageToBoss(target, member.attackDamage)
|
||||
damage = damageResult.healthDamage
|
||||
defeated = damageResult.boss.health <= 0
|
||||
nextBosses = nextBosses.map((boss) => boss.id === target.id
|
||||
@@ -862,13 +887,13 @@ function applyPartyAttacks(
|
||||
return {
|
||||
...member,
|
||||
damageDone: member.damageDone + damage,
|
||||
attackCooldownRemaining: metadata.attackCooldown,
|
||||
attackCooldownRemaining: member.attackCooldown,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...member,
|
||||
damageDone: member.damageDone + damage,
|
||||
attackCooldownRemaining: metadata.attackCooldown,
|
||||
attackCooldownRemaining: member.attackCooldown,
|
||||
}
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Iwt2BossEntityState,
|
||||
Iwt2HostileAddState,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2RoguelikePressureState,
|
||||
} from './types'
|
||||
|
||||
export type Iwt2ArenaEntityKind = 'player' | 'party' | 'boss' | 'projectile' | 'hostileAdd'
|
||||
@@ -58,8 +59,10 @@ export function createInitialIwt2ArenaState(
|
||||
bossId?: Iwt2BossId,
|
||||
bossIds?: Iwt2BossId[],
|
||||
bossHealthScale?: number,
|
||||
partyDamageTakenScale?: number,
|
||||
roguelikePressure?: Iwt2RoguelikePressureState,
|
||||
): Iwt2ArenaState {
|
||||
return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds, bossHealthScale))
|
||||
return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds, bossHealthScale, partyDamageTakenScale, roguelikePressure))
|
||||
}
|
||||
|
||||
export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { tickTobiKadachi } from './tobiKadachiAi'
|
||||
import { tickVenomOrchidHydra } from './venomOrchidHydraAi'
|
||||
import { tickYianKutKu } from './yianKutKuAi'
|
||||
import {
|
||||
applyPartyDamageToMember,
|
||||
applyPartyDamageInShape,
|
||||
createArenaEvent,
|
||||
createCircleIndicator,
|
||||
@@ -281,14 +282,19 @@ function maybeApplyMelee(
|
||||
if (distanceVec2(boss.position, target.position) > BULLDROME_BOSS_METADATA.meleeRange + target.radius) {
|
||||
return { boss, party, events: [] }
|
||||
}
|
||||
let appliedDamage = 0
|
||||
let nextTargetHealth = target.health
|
||||
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 result = applyPartyDamageToMember(member, BULLDROME_BOSS_METADATA.meleeDamage)
|
||||
appliedDamage = result.damage
|
||||
nextTargetHealth = result.member.health
|
||||
return result.member
|
||||
})
|
||||
const events = [
|
||||
createArenaEvent(firstEventId, time, 'partyDamaged', boss.id, target.id, BULLDROME_BOSS_METADATA.meleeDamage),
|
||||
createArenaEvent(firstEventId, time, 'partyDamaged', boss.id, target.id, appliedDamage),
|
||||
]
|
||||
if (target.health > 0 && target.health - BULLDROME_BOSS_METADATA.meleeDamage <= 0) {
|
||||
if (target.health > 0 && nextTargetHealth <= 0) {
|
||||
events.push(createArenaEvent(firstEventId + 1, time, 'entityDefeated', boss.id, target.id))
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -37,22 +37,22 @@ const CINDERBACK_PHASE = {
|
||||
type CinderbackPhase = typeof CINDERBACK_PHASE[keyof typeof CINDERBACK_PHASE]
|
||||
|
||||
const CINDERBACK_TUNING = {
|
||||
armorSlamDamage: 20,
|
||||
armorSlamDamage: 44,
|
||||
armorSlamRadius: 118,
|
||||
armorSlamRecover: 1.05,
|
||||
armorSlamStunSeconds: 0.7,
|
||||
armorSlamWindup: 0.62,
|
||||
centerDisengageSpeed: 152,
|
||||
firePuddleDamage: 5,
|
||||
firePuddleDamage: 15,
|
||||
firePuddleRadius: 34,
|
||||
firePuddleSeconds: 6.4,
|
||||
lavaTrailInterval: 0.26,
|
||||
meleeCooldown: 1.12,
|
||||
meleeDamage: 5,
|
||||
meleeDamage: 11,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 104,
|
||||
ricochetCooldown: 7.2,
|
||||
ricochetDamage: 18,
|
||||
ricochetDamage: 40,
|
||||
ricochetDuration: 3.1,
|
||||
ricochetMaxBounces: 4,
|
||||
ricochetSpeed: 470,
|
||||
|
||||
@@ -44,26 +44,26 @@ const CRYSTAL_BAT_TUNING = {
|
||||
clusterRadius: 84,
|
||||
hoverDistance: 138,
|
||||
meleeCooldown: 1.18,
|
||||
meleeDamage: 4,
|
||||
meleeDamage: 10,
|
||||
meleeRange: 54,
|
||||
moveSpeed: 138,
|
||||
relocateCooldownFloor: 1.2,
|
||||
relocateSeconds: 1.25,
|
||||
relocateSpeed: 184,
|
||||
shardCooldown: 5.8,
|
||||
shardDamage: 13,
|
||||
shardDamage: 34,
|
||||
shardLaneWidth: 30,
|
||||
shardRecover: 0.58,
|
||||
shardStunSeconds: 0.35,
|
||||
shardWindup: 0.82,
|
||||
sonicCooldown: 6.4,
|
||||
sonicDamage: 10,
|
||||
sonicDamage: 30,
|
||||
sonicRecover: 0.42,
|
||||
sonicRingRadius: 58,
|
||||
sonicStunSeconds: 0.32,
|
||||
sonicWindup: 0.78,
|
||||
swoopCooldown: 4.9,
|
||||
swoopDamage: 17,
|
||||
swoopDamage: 42,
|
||||
swoopLaneWidth: 44,
|
||||
swoopRecover: 0.62,
|
||||
swoopSpeed: 430,
|
||||
|
||||
@@ -36,16 +36,16 @@ type EmberMantisAttackPhase =
|
||||
const EMBER_MANTIS = {
|
||||
accentColor: '#ffb13b',
|
||||
crossSlashCooldown: 7.5,
|
||||
crossSlashDamage: 20,
|
||||
crossSlashDamage: 44,
|
||||
crossSlashStunSeconds: 0.45,
|
||||
crossSlashWindup: 0.82,
|
||||
duelistRange: 118,
|
||||
lineSlashCooldown: 3.4,
|
||||
lineSlashDamage: 15,
|
||||
lineSlashDamage: 36,
|
||||
lineSlashStunSeconds: 0.25,
|
||||
lineSlashWindup: 0.48,
|
||||
meleeCooldown: 0.8,
|
||||
meleeDamage: 7,
|
||||
meleeDamage: 12,
|
||||
meleeRange: 52,
|
||||
moveSpeed: 168,
|
||||
recoverSeconds: 0.38,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Iwt2HealerAbility } from '../content/healerAbilities'
|
||||
import {
|
||||
IWT2_GEAR_SLOT_RECIPES,
|
||||
IWT2_GEAR_SLOTS,
|
||||
type Iwt2GearProgress,
|
||||
} from '../content/gear'
|
||||
import type { Iwt2ArenaState, Iwt2PartyEntityState } from './types'
|
||||
import { applyIwt2InfusionLoadout } from './infusionLoadout'
|
||||
|
||||
const PER_LEVEL_OUTPUT = 0.04
|
||||
const PER_LEVEL_HEALTH = 0.04
|
||||
const PER_LEVEL_SPEED = 0.025
|
||||
const PER_LEVEL_COOLDOWN = 0.03
|
||||
const PER_LEVEL_PROJECTILE = 0.03
|
||||
const PER_LEVEL_DAMAGE_TAKEN_REDUCTION = 0.03
|
||||
const PER_LEVEL_STUN_REDUCTION = 0.08
|
||||
|
||||
export function applyIwt2PveGearStats<TState extends Pick<Iwt2ArenaState, 'party'>>(state: TState, gearProgress: Iwt2GearProgress): TState {
|
||||
const party = state.party.map((member) => applyMemberGearStats(member, gearProgress))
|
||||
return applyIwt2InfusionLoadout({
|
||||
...state,
|
||||
party,
|
||||
}, gearProgress)
|
||||
}
|
||||
|
||||
export function applyIwt2PveGearToHealerAbilities(
|
||||
abilities: Iwt2HealerAbility[],
|
||||
gearProgress: Iwt2GearProgress,
|
||||
): Iwt2HealerAbility[] {
|
||||
const healer = gearProgress.healer
|
||||
const healingLevel = healer.slots.weapon.level
|
||||
const cooldownLevel = healer.slots.helmet.level
|
||||
if (healingLevel <= 0 && cooldownLevel <= 0) return abilities
|
||||
return abilities.map((ability) => ({
|
||||
...ability,
|
||||
cooldownSeconds: roundOne(ability.cooldownSeconds * (1 - cooldownLevel * PER_LEVEL_COOLDOWN)),
|
||||
power: Math.max(1, Math.round(ability.power * (1 + healingLevel * PER_LEVEL_OUTPUT))),
|
||||
}))
|
||||
}
|
||||
|
||||
function applyMemberGearStats(
|
||||
member: Iwt2PartyEntityState,
|
||||
gearProgress: Iwt2GearProgress,
|
||||
): Iwt2PartyEntityState {
|
||||
const classProgress = gearProgress[member.classId]
|
||||
let next = { ...member }
|
||||
for (const slotId of IWT2_GEAR_SLOTS) {
|
||||
const level = classProgress.slots[slotId].level
|
||||
if (level <= 0) continue
|
||||
const statId = IWT2_GEAR_SLOT_RECIPES[member.classId][slotId].statId
|
||||
if (statId === 'maxHealth') {
|
||||
const maxHealth = Math.round(next.maxHealth * (1 + level * PER_LEVEL_HEALTH))
|
||||
next = {
|
||||
...next,
|
||||
health: Math.min(maxHealth, Math.round(next.health * (maxHealth / next.maxHealth))),
|
||||
maxHealth,
|
||||
}
|
||||
} else if (statId === 'moveSpeed') {
|
||||
next = { ...next, moveSpeed: next.moveSpeed * (1 + level * PER_LEVEL_SPEED) }
|
||||
} else if (statId === 'damage') {
|
||||
next = { ...next, attackDamage: next.attackDamage * (1 + level * PER_LEVEL_OUTPUT) }
|
||||
} else if (statId === 'attackCooldown') {
|
||||
next = { ...next, attackCooldown: next.attackCooldown * (1 - level * PER_LEVEL_COOLDOWN) }
|
||||
} else if (statId === 'projectileSpeed') {
|
||||
next = { ...next, projectileSpeed: next.projectileSpeed * (1 + level * PER_LEVEL_PROJECTILE) }
|
||||
} else if (statId === 'hazardDamageTaken') {
|
||||
next = { ...next, damageTakenScale: next.damageTakenScale * (1 - level * PER_LEVEL_DAMAGE_TAKEN_REDUCTION) }
|
||||
} else if (statId === 'stunResist') {
|
||||
next = { ...next, stunTakenScale: Math.max(0, next.stunTakenScale * (1 - level * PER_LEVEL_STUN_REDUCTION)) }
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function roundOne(value: number): number {
|
||||
return Math.max(0.1, Math.round(value * 10) / 10)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Iwt2HealerAbility } from '../content/healerAbilities'
|
||||
import type { Iwt2HealerAbility, Iwt2TriggeredAbilityEffect } from '../content/healerAbilities'
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2ArenaState,
|
||||
@@ -31,6 +31,8 @@ export function castIwt2HealerAbility<TState extends Iwt2ArenaState>(
|
||||
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
let eventId = state.nextEventId
|
||||
events.push(createArenaEvent(eventId, state.time, 'healerSpellCast', HEALER_ID, targetIds[0]))
|
||||
eventId += 1
|
||||
const targetIdSet = new Set(targetIds)
|
||||
const nextParty = state.party.map((member) => {
|
||||
const mana = member.id === HEALER_ID ? Math.max(0, member.mana - ability.manaCost) : member.mana
|
||||
@@ -82,6 +84,12 @@ function targetIdsForAbility(
|
||||
}
|
||||
|
||||
function applyAbilityToMember(member: Iwt2PartyEntityState, ability: Iwt2HealerAbility): Iwt2PartyEntityState {
|
||||
const baseMember = applyAbilityCoreToMember(member, ability)
|
||||
if (!ability.triggeredEffects || ability.triggeredEffects.length === 0) return baseMember
|
||||
return ability.triggeredEffects.reduce(applyTriggeredEffectToMember, baseMember)
|
||||
}
|
||||
|
||||
function applyAbilityCoreToMember(member: Iwt2PartyEntityState, ability: Iwt2HealerAbility): Iwt2PartyEntityState {
|
||||
if (ability.kind === 'hot') {
|
||||
return {
|
||||
...healMember(member, ability.power),
|
||||
@@ -120,6 +128,37 @@ function applyAbilityToMember(member: Iwt2PartyEntityState, ability: Iwt2HealerA
|
||||
return healMember(member, ability.power)
|
||||
}
|
||||
|
||||
function applyTriggeredEffectToMember(
|
||||
member: Iwt2PartyEntityState,
|
||||
effect: Iwt2TriggeredAbilityEffect,
|
||||
): Iwt2PartyEntityState {
|
||||
if (effect.kind === 'hot') {
|
||||
return {
|
||||
...healMember(member, effect.power),
|
||||
hotEffects: [
|
||||
...member.hotEffects.filter((hotEffect) => hotEffect.id !== effect.id),
|
||||
{
|
||||
id: effect.id,
|
||||
label: effect.name,
|
||||
power: Math.max(1, Math.round(effect.power * 0.5)),
|
||||
remainingSeconds: DEFAULT_HOT_SECONDS,
|
||||
tickIntervalSeconds: DEFAULT_HOT_TICK_SECONDS,
|
||||
nextTickInSeconds: DEFAULT_HOT_TICK_SECONDS,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (effect.kind === 'shield') {
|
||||
return {
|
||||
...member,
|
||||
shield: Math.max(member.shield, effect.power),
|
||||
}
|
||||
}
|
||||
|
||||
return healMember(member, effect.power)
|
||||
}
|
||||
|
||||
function healMember(member: Iwt2PartyEntityState, amount: number): Iwt2PartyEntityState {
|
||||
return {
|
||||
...member,
|
||||
|
||||
@@ -44,7 +44,7 @@ type HollowcrownAttackPhase = Iwt2BossEntityState['attackPhase'] | HollowcrownSp
|
||||
|
||||
const HOLLOWCROWN_TUNING = {
|
||||
antlerCooldown: 7.4,
|
||||
antlerDamage: 17,
|
||||
antlerDamage: 42,
|
||||
antlerLaneLength: 440,
|
||||
antlerLaneOffset: 44,
|
||||
antlerLineWidth: 30,
|
||||
@@ -52,22 +52,22 @@ const HOLLOWCROWN_TUNING = {
|
||||
antlerStunSeconds: 0.48,
|
||||
antlerWindup: 0.82,
|
||||
dashCooldown: 5.2,
|
||||
dashDamage: 13,
|
||||
dashDamage: 34,
|
||||
dashLength: 330,
|
||||
dashRecover: 0.42,
|
||||
dashSpeed: 430,
|
||||
dashStunSeconds: 0.32,
|
||||
dashTrailDamage: 9,
|
||||
dashTrailDamage: 20,
|
||||
dashTrailSeconds: 0.5,
|
||||
dashTrailWidth: 40,
|
||||
hoofCooldown: 6.2,
|
||||
hoofDamage: 12,
|
||||
hoofDamage: 34,
|
||||
hoofRadius: 46,
|
||||
hoofRecover: 0.46,
|
||||
hoofStunSeconds: 0.34,
|
||||
hoofWindup: 0.74,
|
||||
meleeCooldown: 1.25,
|
||||
meleeDamage: 5,
|
||||
meleeDamage: 11,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 116,
|
||||
relocateSeconds: 1.25,
|
||||
|
||||
@@ -27,6 +27,11 @@ export {
|
||||
castIwt2HealerAbility,
|
||||
} from './healing'
|
||||
|
||||
export {
|
||||
applyIwt2InfusionLoadout,
|
||||
selectedInfusionForMember,
|
||||
} from './infusionLoadout'
|
||||
|
||||
export {
|
||||
addVec2,
|
||||
clamp,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IWT2_INFUSION_ABILITIES, type Iwt2InfusionAbility } from '../content/infusionAbilities'
|
||||
import type { Iwt2GearProgress } from '../content/gear'
|
||||
import type { Iwt2ArenaState, Iwt2PartyEntityState } from './types'
|
||||
|
||||
export function applyIwt2InfusionLoadout<TState extends Pick<Iwt2ArenaState, 'party'>>(
|
||||
state: TState,
|
||||
gearProgress: Iwt2GearProgress,
|
||||
): TState {
|
||||
return {
|
||||
...state,
|
||||
party: state.party.map((member) => {
|
||||
const ability = selectedInfusionForMember(member, gearProgress)
|
||||
return ability ? applyInfusionToMember(member, ability) : member
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function selectedInfusionForMember(
|
||||
member: Pick<Iwt2PartyEntityState, 'classId'>,
|
||||
gearProgress: Iwt2GearProgress,
|
||||
): Iwt2InfusionAbility | null {
|
||||
const abilityId = gearProgress[member.classId].infusionAbilityId
|
||||
if (!abilityId) return null
|
||||
const ability = IWT2_INFUSION_ABILITIES[abilityId]
|
||||
return ability?.classId === member.classId ? ability : null
|
||||
}
|
||||
|
||||
function applyInfusionToMember(
|
||||
member: Iwt2PartyEntityState,
|
||||
ability: Iwt2InfusionAbility,
|
||||
): Iwt2PartyEntityState {
|
||||
const baseMember = {
|
||||
...member,
|
||||
infusionAbilityId: ability.id,
|
||||
}
|
||||
if (ability.effectKey === 'unbreakable') {
|
||||
return {
|
||||
...baseMember,
|
||||
stunTakenScale: 0,
|
||||
}
|
||||
}
|
||||
return baseMember
|
||||
}
|
||||
@@ -127,25 +127,22 @@ export function applyPartyDamageInShape(
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const excluded = new Set(effect.excludedEntityIds ?? [])
|
||||
const hitEntityIds: Iwt2EntityId[] = []
|
||||
const stunSeconds = effect.stunSeconds ?? 0
|
||||
const knockdownSeconds = effect.knockdownSeconds ?? stunSeconds
|
||||
const nextParty = party.map((member) => {
|
||||
if (member.health <= 0 || excluded.has(member.id) || !partyMemberIntersectsShape(member, shape)) {
|
||||
return member
|
||||
}
|
||||
|
||||
const stunSeconds = (effect.stunSeconds ?? 0) * member.stunTakenScale
|
||||
const knockdownSeconds = (effect.knockdownSeconds ?? (effect.stunSeconds ?? 0)) * member.stunTakenScale
|
||||
const damageResult = applyPartyDamageToMember(member, effect.damage)
|
||||
hitEntityIds.push(member.id)
|
||||
const shieldAbsorbed = Math.min(member.shield, effect.damage)
|
||||
const healthDamage = effect.damage - shieldAbsorbed
|
||||
const nextShield = member.shield - shieldAbsorbed
|
||||
const nextHealth = Math.max(0, member.health - healthDamage)
|
||||
events.push(createArenaEvent(
|
||||
0,
|
||||
effect.time,
|
||||
effect.damageEventType ?? 'partyDamaged',
|
||||
effect.sourceId,
|
||||
member.id,
|
||||
effect.damage,
|
||||
damageResult.damage,
|
||||
))
|
||||
if (stunSeconds > 0 || knockdownSeconds > 0) {
|
||||
events.push(createArenaEvent(
|
||||
@@ -157,14 +154,12 @@ export function applyPartyDamageInShape(
|
||||
Math.max(stunSeconds, knockdownSeconds),
|
||||
))
|
||||
}
|
||||
if (member.health > 0 && nextHealth <= 0) {
|
||||
if (member.health > 0 && damageResult.member.health <= 0) {
|
||||
events.push(createArenaEvent(0, effect.time, 'entityDefeated', effect.sourceId, member.id))
|
||||
}
|
||||
|
||||
return {
|
||||
...member,
|
||||
health: nextHealth,
|
||||
shield: nextShield,
|
||||
...damageResult.member,
|
||||
status: {
|
||||
...member.status,
|
||||
stunnedSeconds: Math.max(member.status.stunnedSeconds, stunSeconds),
|
||||
@@ -176,6 +171,29 @@ export function applyPartyDamageInShape(
|
||||
return { party: nextParty, hitEntityIds, events }
|
||||
}
|
||||
|
||||
export function applyPartyDamageToMember(
|
||||
member: Iwt2PartyEntityState,
|
||||
damage: number,
|
||||
): {
|
||||
member: Iwt2PartyEntityState
|
||||
damage: number
|
||||
} {
|
||||
const shieldedDamageMultiplier = member.shield > 0
|
||||
? member.shieldedDamageTakenMultiplier ?? 1
|
||||
: 1
|
||||
const scaledDamage = Math.max(0, damage * member.damageTakenScale * shieldedDamageMultiplier)
|
||||
const shieldAbsorbed = Math.min(member.shield, scaledDamage)
|
||||
const healthDamage = scaledDamage - shieldAbsorbed
|
||||
return {
|
||||
damage: scaledDamage,
|
||||
member: {
|
||||
...member,
|
||||
health: Math.max(0, member.health - healthDamage),
|
||||
shield: member.shield - shieldAbsorbed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function partyMemberIntersectsShape(member: Iwt2PartyEntityState, shape: Iwt2HitShape): boolean {
|
||||
if (shape.kind === 'lane') {
|
||||
return circleIntersectsSegment(
|
||||
|
||||
@@ -47,7 +47,7 @@ const OBSIDIAN_RAM = {
|
||||
armorCracksBeforeShatter: 3,
|
||||
centerDisengageSpeed: 118,
|
||||
chargeCooldown: 4.4,
|
||||
chargeDamage: 8,
|
||||
chargeDamage: 24,
|
||||
chargeLength: 340,
|
||||
chargeRecover: 0.5,
|
||||
chargeSpeed: 390,
|
||||
@@ -55,11 +55,11 @@ const OBSIDIAN_RAM = {
|
||||
chargeWidth: 54,
|
||||
chargeWindup: 0.68,
|
||||
fractureLaneCount: 5,
|
||||
fractureLaneDamage: 6,
|
||||
fractureLaneDamage: 20,
|
||||
fractureLaneLength: 300,
|
||||
fractureLaneWidth: 30,
|
||||
meleeCooldown: 1.3,
|
||||
meleeDamage: 2,
|
||||
meleeDamage: 8,
|
||||
meleeRange: 62,
|
||||
moveSpeed: 82,
|
||||
quakeCooldown: 7.4,
|
||||
@@ -67,7 +67,7 @@ const OBSIDIAN_RAM = {
|
||||
quakeRecover: 0.48,
|
||||
quakeStunSeconds: 0.35,
|
||||
quakeWindup: 0.78,
|
||||
slabDamage: 10,
|
||||
slabDamage: 22,
|
||||
slabDuration: 5.8,
|
||||
slabRadius: 42,
|
||||
slabSlowRadius: 46,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { IWT2_CLASS_METADATA } from '../content/classes'
|
||||
import { BULLDROME_BOSS_METADATA, IWT2_BOSS_METADATA } from '../content/bosses'
|
||||
import type { Iwt2ArenaState, Iwt2HostileAddState, Iwt2PartyEntityState, Iwt2Vec2 } from './types'
|
||||
import { partyMemberIntersectsShape, type Iwt2HitShape } from './mechanics'
|
||||
@@ -34,7 +33,6 @@ export function tickPartyMember(
|
||||
inputMove: Iwt2Vec2,
|
||||
dt: number,
|
||||
): Iwt2PartyEntityState {
|
||||
const metadata = IWT2_CLASS_METADATA[member.classId]
|
||||
const status = {
|
||||
stunnedSeconds: Math.max(0, member.status.stunnedSeconds - dt),
|
||||
knockedDownSeconds: Math.max(0, member.status.knockedDownSeconds - dt),
|
||||
@@ -52,7 +50,7 @@ export function tickPartyMember(
|
||||
|
||||
if (member.aiRole === 'player') {
|
||||
const normalizedInput = lengthSqVec2(inputMove) > 1 ? normalizeVec2(inputMove) : inputMove
|
||||
const velocity = scaleVec2(normalizedInput, metadata.moveSpeed * movementSpeedMultiplier(status.slowedSeconds))
|
||||
const velocity = scaleVec2(normalizedInput, member.moveSpeed * movementSpeedMultiplier(status.slowedSeconds))
|
||||
const position = clampVec2ToArena(
|
||||
{
|
||||
x: member.position.x + velocity.x * dt,
|
||||
@@ -82,7 +80,7 @@ export function tickPartyMember(
|
||||
if (!dangerDestination && member.aiRole === 'ranged' && (member.castSecondsRemaining > 0 || canCast)) {
|
||||
const nextCastSecondsRemaining = member.castSecondsRemaining > 0
|
||||
? castSecondsRemaining
|
||||
: metadata.castTime
|
||||
: member.castTime
|
||||
return {
|
||||
...member,
|
||||
velocity: { x: 0, y: 0 },
|
||||
@@ -96,7 +94,7 @@ export function tickPartyMember(
|
||||
|
||||
const decisionSecondsRemaining = Math.max(0, member.decisionSecondsRemaining - dt)
|
||||
const desired = dangerDestination ?? getPartyDesiredPosition(member, state)
|
||||
const maxDistance = metadata.moveSpeed * movementSpeedMultiplier(status.slowedSeconds) * dt
|
||||
const maxDistance = member.moveSpeed * movementSpeedMultiplier(status.slowedSeconds) * dt
|
||||
const distanceToDesired = distanceVec2(member.position, desired)
|
||||
const movingToDesired = distanceToDesired > PARTY_ARRIVAL_RADIUS
|
||||
const position = movingToDesired
|
||||
@@ -229,9 +227,8 @@ function rotateVec2(vector: Iwt2Vec2, angle: number): Iwt2Vec2 {
|
||||
export function canPartyMemberHitTarget(member: Iwt2PartyEntityState, targetPosition: Iwt2Vec2, targetRadius = 0): boolean {
|
||||
if (member.health <= 0) return false
|
||||
if (member.status.stunnedSeconds > 0 || member.status.knockedDownSeconds > 0) return false
|
||||
const metadata = IWT2_CLASS_METADATA[member.classId]
|
||||
if (metadata.attackDamage <= 0) return false
|
||||
return distanceVec2(member.position, targetPosition) <= metadata.attackRange + member.radius + targetRadius
|
||||
if (member.attackDamage <= 0) return false
|
||||
return distanceVec2(member.position, targetPosition) <= member.attackRange + member.radius + targetRadius
|
||||
}
|
||||
|
||||
function getPartyDesiredPosition(
|
||||
|
||||
@@ -49,7 +49,7 @@ type ShardCone = {
|
||||
|
||||
const RIMEBASTION_BALANCE = {
|
||||
iceWallCooldown: 7.2,
|
||||
iceWallDamage: 6,
|
||||
iceWallDamage: 18,
|
||||
iceWallStunSeconds: 0.25,
|
||||
iceWallWindup: 1,
|
||||
iceCircleCount: 2,
|
||||
@@ -57,16 +57,16 @@ const RIMEBASTION_BALANCE = {
|
||||
iceWallLaneCount: 3,
|
||||
iceWallWidth: 22,
|
||||
meleeCooldown: 1.45,
|
||||
meleeDamage: 4,
|
||||
meleeDamage: 10,
|
||||
meleeRange: 66,
|
||||
moveSpeed: 64,
|
||||
relocateCooldownFloor: 1.3,
|
||||
relocateSpeed: 112,
|
||||
relocateSeconds: 1.6,
|
||||
shatterConeAngleRadians: Math.PI * 0.34,
|
||||
shatterConeDamage: 9,
|
||||
shatterConeDamage: 30,
|
||||
shatterConeRange: 158,
|
||||
shatterLaneDamage: 11,
|
||||
shatterLaneDamage: 34,
|
||||
shatterLaneWidth: 34,
|
||||
shatterRecover: 0.52,
|
||||
shatterStunSeconds: 0.4,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2RoguelikePressureContentType,
|
||||
Iwt2RoguelikePressureState,
|
||||
} from './types'
|
||||
import { applyPartyDamageToMember, createArenaEvent } from './mechanics'
|
||||
|
||||
const ROGUELIKE_PRESSURE_INTERVAL_SECONDS = 5
|
||||
const ROGUELIKE_PRESSURE_BASE_DAMAGE = 9
|
||||
const ROGUELIKE_PRESSURE_INITIAL_STAGE_DAMAGE = 2
|
||||
const ROGUELIKE_DUNGEON_DAMAGE_BASE_SCALE = 1.25
|
||||
const ROGUELIKE_RAID_DAMAGE_BASE_SCALE = 1.45
|
||||
const ROGUELIKE_PRESSURE_SOURCE_ID = 'roguelike-pressure'
|
||||
|
||||
export function roguelikeIncomingDamageScale(
|
||||
_stage: number,
|
||||
contentType: Iwt2RoguelikePressureContentType,
|
||||
): number {
|
||||
const base = contentType === 'dungeon'
|
||||
? ROGUELIKE_DUNGEON_DAMAGE_BASE_SCALE
|
||||
: ROGUELIKE_RAID_DAMAGE_BASE_SCALE
|
||||
return base
|
||||
}
|
||||
|
||||
export function createRoguelikePressureState(
|
||||
stage: number,
|
||||
contentType: Iwt2RoguelikePressureContentType,
|
||||
): Iwt2RoguelikePressureState {
|
||||
const safeStage = safeRoguelikeStage(stage)
|
||||
return {
|
||||
stage: safeStage,
|
||||
contentType,
|
||||
damage: Math.round(ROGUELIKE_PRESSURE_BASE_DAMAGE + ROGUELIKE_PRESSURE_INITIAL_STAGE_DAMAGE),
|
||||
intervalSeconds: ROGUELIKE_PRESSURE_INTERVAL_SECONDS,
|
||||
nextPulseAt: ROGUELIKE_PRESSURE_INTERVAL_SECONDS,
|
||||
}
|
||||
}
|
||||
|
||||
export function tickRoguelikePressure({
|
||||
party,
|
||||
pressure,
|
||||
time,
|
||||
}: {
|
||||
party: Iwt2PartyEntityState[]
|
||||
pressure?: Iwt2RoguelikePressureState
|
||||
time: number
|
||||
}): {
|
||||
events: Iwt2ArenaEvent[]
|
||||
party: Iwt2PartyEntityState[]
|
||||
pressure?: Iwt2RoguelikePressureState
|
||||
} {
|
||||
if (!pressure || time < pressure.nextPulseAt) {
|
||||
return { events: [], party, pressure }
|
||||
}
|
||||
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const nextParty = party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
|
||||
const before = member.health
|
||||
const result = applyPartyDamageToMember(member, pressure.damage)
|
||||
events.push(createArenaEvent(
|
||||
0,
|
||||
time,
|
||||
'partyDamaged',
|
||||
ROGUELIKE_PRESSURE_SOURCE_ID,
|
||||
member.id,
|
||||
result.damage,
|
||||
))
|
||||
if (before > 0 && result.member.health <= 0) {
|
||||
events.push(createArenaEvent(0, time, 'entityDefeated', ROGUELIKE_PRESSURE_SOURCE_ID, member.id))
|
||||
}
|
||||
return result.member
|
||||
})
|
||||
|
||||
let nextPulseAt = pressure.nextPulseAt
|
||||
while (nextPulseAt <= time) nextPulseAt += pressure.intervalSeconds
|
||||
|
||||
return {
|
||||
events,
|
||||
party: nextParty,
|
||||
pressure: {
|
||||
...pressure,
|
||||
nextPulseAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function safeRoguelikeStage(stage: number): number {
|
||||
return Number.isFinite(stage) ? Math.max(1, Math.floor(stage)) : 1
|
||||
}
|
||||
@@ -50,20 +50,20 @@ const SANDGLASS = {
|
||||
burrowWindup: 0.48,
|
||||
disengageSeconds: 0.72,
|
||||
disengageSpeed: 148,
|
||||
eruptionDamage: 15,
|
||||
eruptionDamage: 70,
|
||||
eruptionRadius: 50,
|
||||
eruptionRecover: 0.52,
|
||||
eruptionStunSeconds: 0.42,
|
||||
eruptionWindup: 0.76,
|
||||
hourglassCooldown: 8.1,
|
||||
hourglassDamage: 6,
|
||||
hourglassDamage: 35,
|
||||
hourglassHazardSeconds: 2.2,
|
||||
hourglassRadius: 66,
|
||||
hourglassRecover: 0.48,
|
||||
hourglassSafeSeconds: 1.05,
|
||||
hourglassTickSeconds: 0.5,
|
||||
meleeCooldown: 1.25,
|
||||
meleeDamage: 5,
|
||||
meleeDamage: 22,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 94,
|
||||
wallMargin: 70,
|
||||
|
||||
@@ -41,14 +41,14 @@ type StormcoilPhase = typeof STORMCOIL_PHASE[keyof typeof STORMCOIL_PHASE]
|
||||
const STORMCOIL_TUNING = {
|
||||
arcAngleRadians: Math.PI * 0.34,
|
||||
arcCooldown: 5.6,
|
||||
arcDamage: 6,
|
||||
arcDamage: 22,
|
||||
arcInnerRadius: 78,
|
||||
arcOuterRadius: 196,
|
||||
arcRecover: 0.46,
|
||||
arcStunSeconds: 0.24,
|
||||
arcWindup: 0.74,
|
||||
chainCooldown: 7.4,
|
||||
chainDamage: 5,
|
||||
chainDamage: 18,
|
||||
chainLinkRadius: 150,
|
||||
chainMaxLinks: 7,
|
||||
chainRecover: 0.5,
|
||||
@@ -57,14 +57,14 @@ const STORMCOIL_TUNING = {
|
||||
chainWindup: 0.68,
|
||||
disengageSpeed: 168,
|
||||
donutCooldown: 6.2,
|
||||
donutDamage: 7,
|
||||
donutDamage: 24,
|
||||
donutInnerRadius: 64,
|
||||
donutOuterRadius: 156,
|
||||
donutRecover: 0.48,
|
||||
donutStunSeconds: 0.44,
|
||||
donutWindup: 0.82,
|
||||
meleeCooldown: 1.12,
|
||||
meleeDamage: 2,
|
||||
meleeDamage: 8,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 124,
|
||||
wallContactLimit: 0.72,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Iwt2BossId } from '../content/bosses'
|
||||
import type { Iwt2PlayerClassId } from '../content/classes'
|
||||
import type { Iwt2InfusionAbilityId } from '../content/infusionAbilities'
|
||||
|
||||
export type Iwt2Vec2 = {
|
||||
x: number
|
||||
@@ -47,13 +48,24 @@ export type Iwt2PartyEntityState = {
|
||||
id: Iwt2PartyEntityId
|
||||
kind: 'party'
|
||||
classId: Iwt2PlayerClassId
|
||||
infusionAbilityId?: Iwt2InfusionAbilityId
|
||||
aiRole: Iwt2PartyAiRole
|
||||
position: Iwt2Vec2
|
||||
velocity: Iwt2Vec2
|
||||
facing: Iwt2Vec2
|
||||
radius: number
|
||||
moveSpeed: number
|
||||
attackRange: number
|
||||
attackDamage: number
|
||||
attackCooldown: number
|
||||
castTime: number
|
||||
projectileSpeed: number
|
||||
health: number
|
||||
maxHealth: number
|
||||
damageTakenScale: number
|
||||
stunTakenScale: number
|
||||
shieldedDamageTakenMultiplier?: number
|
||||
shieldedHotHealingMultiplier?: number
|
||||
shield: number
|
||||
mana: number
|
||||
maxMana: number
|
||||
@@ -270,6 +282,7 @@ export type Iwt2GroundHazardState = {
|
||||
|
||||
export type Iwt2ArenaEventType =
|
||||
| 'bossDamaged'
|
||||
| 'healerSpellCast'
|
||||
| 'partyHealed'
|
||||
| 'partyDamaged'
|
||||
| 'partyStunned'
|
||||
@@ -287,6 +300,16 @@ export type Iwt2ArenaEvent = {
|
||||
value?: number
|
||||
}
|
||||
|
||||
export type Iwt2RoguelikePressureContentType = 'dungeon' | 'raid' | 'stadium'
|
||||
|
||||
export type Iwt2RoguelikePressureState = {
|
||||
stage: number
|
||||
contentType: Iwt2RoguelikePressureContentType
|
||||
intervalSeconds: number
|
||||
damage: number
|
||||
nextPulseAt: number
|
||||
}
|
||||
|
||||
export type Iwt2ArenaIndicatorPhase = 'windup' | 'active' | 'recover'
|
||||
|
||||
export type Iwt2ArenaIndicatorBase = {
|
||||
@@ -354,6 +377,7 @@ export type Iwt2ArenaState = {
|
||||
boss: Iwt2BossEntityState
|
||||
bosses: Iwt2BossEntityState[]
|
||||
indicators: Iwt2ArenaIndicator[]
|
||||
roguelikePressure?: Iwt2RoguelikePressureState
|
||||
nextEventId: number
|
||||
nextProjectileId: number
|
||||
nextAddId: number
|
||||
|
||||
@@ -42,29 +42,29 @@ const VENOM_ORCHID_HYDRA_PHASE = {
|
||||
type VenomOrchidHydraPhase = typeof VENOM_ORCHID_HYDRA_PHASE[keyof typeof VENOM_ORCHID_HYDRA_PHASE]
|
||||
|
||||
const VENOM_ORCHID_HYDRA = {
|
||||
bloomBurstDamage: 7,
|
||||
bloomBurstDamage: 18,
|
||||
bloomCooldown: 8.6,
|
||||
bloomPodCount: 3,
|
||||
bloomPodDamage: 3,
|
||||
bloomPodDamage: 10,
|
||||
bloomPodRadius: 30,
|
||||
bloomPodSeconds: 1.4,
|
||||
bloomRecover: 0.5,
|
||||
bloomWindup: 0.9,
|
||||
coneAngleRadians: Math.PI * 0.34,
|
||||
coneCooldown: 5.7,
|
||||
coneDamage: 10,
|
||||
coneDamage: 28,
|
||||
coneRange: 178,
|
||||
coneRecover: 0.62,
|
||||
coneStaggerWindup: 0.48,
|
||||
coneStunSeconds: 0.18,
|
||||
headOffset: 22,
|
||||
meleeCooldown: 1.35,
|
||||
meleeDamage: 4,
|
||||
meleeDamage: 10,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 58,
|
||||
relocateSpeed: 114,
|
||||
relocateSeconds: 1.45,
|
||||
rootLaneDamage: 8,
|
||||
rootLaneDamage: 22,
|
||||
rootLaneLengthInset: 36,
|
||||
rootLaneWidth: 28,
|
||||
rootRecover: 0.58,
|
||||
|
||||
@@ -45,7 +45,7 @@ export type Iwt2YianTickResult = {
|
||||
const BIRD_COUNT = 3
|
||||
const BIRD_MELEE_RANGE = 24
|
||||
const BIRD_MELEE_COOLDOWN = 1.15
|
||||
const BIRD_FLIGHT_DAMAGE = 10
|
||||
const BIRD_FLIGHT_DAMAGE = 26
|
||||
const YIAN_FIREBALL_BOUNCES = 8
|
||||
const YIAN_SAFE_WALL_MARGIN = 118
|
||||
const YIAN_CENTER_CAST_DISTANCE = 36
|
||||
|
||||
Reference in New Issue
Block a user