Android build v1.1.20
This commit is contained in:
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.warren.iwanttoheal"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 98
|
versionCode 99
|
||||||
versionName "1.1.19"
|
versionName "1.1.20"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -65,9 +65,10 @@ type BossArenaScreenProps = {
|
|||||||
export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
|
export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
|
||||||
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
||||||
const pvpRoguelike = roguelikeRun?.variant === 'pvp'
|
const pvpRoguelike = roguelikeRun?.variant === 'pvp'
|
||||||
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId, bossIds))
|
const bossHealthScale = roguelikeRun ? roguelikeBossHealthScale(roguelikeRun.stage) : 1
|
||||||
|
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale))
|
||||||
const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
|
const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
|
||||||
pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds) : null
|
pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale) : null
|
||||||
))
|
))
|
||||||
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
|
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
|
||||||
const [status, setStatus] = useState<ArenaStatus>('playing')
|
const [status, setStatus] = useState<ArenaStatus>('playing')
|
||||||
@@ -118,8 +119,8 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
|||||||
}, [save])
|
}, [save])
|
||||||
|
|
||||||
const resetArena = useCallback(() => {
|
const resetArena = useCallback(() => {
|
||||||
const next = createInitialIwt2ArenaState(bossId, bossIds)
|
const next = createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale)
|
||||||
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds) : null
|
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale) : null
|
||||||
recordedKillIdsRef.current = new Set()
|
recordedKillIdsRef.current = new Set()
|
||||||
abilityCooldownsRef.current = {}
|
abilityCooldownsRef.current = {}
|
||||||
lastHudSignatureRef.current = arenaHudSignature(next)
|
lastHudSignatureRef.current = arenaHudSignature(next)
|
||||||
@@ -130,7 +131,7 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
|||||||
setAbilityCooldowns({})
|
setAbilityCooldowns({})
|
||||||
setSelectedOverlayAction('primary')
|
setSelectedOverlayAction('primary')
|
||||||
setStatus('playing')
|
setStatus('playing')
|
||||||
}, [bossId, bossIds, pvpRoguelike])
|
}, [bossHealthScale, bossId, bossIds, pvpRoguelike])
|
||||||
|
|
||||||
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
|
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
|
||||||
setSelectedOverlayAction('primary')
|
setSelectedOverlayAction('primary')
|
||||||
@@ -206,7 +207,6 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
|||||||
}
|
}
|
||||||
if (action === 'back') {
|
if (action === 'back') {
|
||||||
if (statusRef.current === 'paused') setStatus('playing')
|
if (statusRef.current === 'paused') setStatus('playing')
|
||||||
else onBack()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,7 +481,7 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{lastDevice === 'controller' && (
|
{lastDevice === 'controller' && (
|
||||||
<small className="iwt2-result-hint">D-pad selects, A confirms, B exits</small>
|
<small className="iwt2-result-hint">D-pad selects, A confirms</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -506,6 +506,10 @@ function formatArenaTime(seconds: number): string {
|
|||||||
return `${minutes}:${remainder.toString().padStart(2, '0')}`
|
return `${minutes}:${remainder.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function roguelikeBossHealthScale(stage: number): number {
|
||||||
|
return 0.5 + Math.max(0, stage - 1) * 0.1
|
||||||
|
}
|
||||||
|
|
||||||
function arenaHudSignature(state: Iwt2ArenaState): string {
|
function arenaHudSignature(state: Iwt2ArenaState): string {
|
||||||
return [
|
return [
|
||||||
...state.bosses.map((boss) => [
|
...state.bosses.map((boss) => [
|
||||||
|
|||||||
@@ -665,9 +665,7 @@ export function Iwt2RoguelikeUpgradeScreen({
|
|||||||
setSelectedIndex((current) => moveUpgradeSelection(entries, current, action))
|
setSelectedIndex((current) => moveUpgradeSelection(entries, current, action))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action === 'back' || action === 'pause') {
|
if (action === 'back' || action === 'pause') return
|
||||||
onBack()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -956,10 +954,8 @@ function nextEnabledIndex(actions: Iwt2NavAction[], current: number, direction:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activeUpgradeEntry(entries: Iwt2UpgradeNavEntry[], index: number) {
|
function activeUpgradeEntry(entries: Iwt2UpgradeNavEntry[], index: number) {
|
||||||
const bounded = Math.min(index, entries.length - 1)
|
if (entries.length === 0) return undefined
|
||||||
const active = entries[bounded]
|
return entries[Math.min(index, entries.length - 1)]
|
||||||
if (active && !upgradeEntryDisabled(active)) return active
|
|
||||||
return entries.find((entry) => !upgradeEntryDisabled(entry))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeEntryDisabled(entry: Iwt2UpgradeNavEntry) {
|
function upgradeEntryDisabled(entry: Iwt2UpgradeNavEntry) {
|
||||||
@@ -967,30 +963,17 @@ function upgradeEntryDisabled(entry: Iwt2UpgradeNavEntry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function moveUpgradeSelection(entries: Iwt2UpgradeNavEntry[], current: number, action: string) {
|
function moveUpgradeSelection(entries: Iwt2UpgradeNavEntry[], current: number, action: string) {
|
||||||
const enabledEntries = entries
|
if (entries.length === 0) return 0
|
||||||
.map((entry, index) => ({ entry, index }))
|
|
||||||
.filter(({ entry }) => !upgradeEntryDisabled(entry))
|
|
||||||
if (enabledEntries.length === 0) return 0
|
|
||||||
const bounded = Math.min(current, entries.length - 1)
|
const bounded = Math.min(current, entries.length - 1)
|
||||||
const candidate = entries[bounded]
|
const active = entries[bounded]
|
||||||
const active = candidate && !upgradeEntryDisabled(candidate)
|
const continueIndex = entries.findIndex((entry) => entry.kind === 'upgradeContinue' || entry.kind === 'upgradeLeave')
|
||||||
? candidate
|
const lastChoiceIndex = Math.max(0, continueIndex >= 0 ? continueIndex - 1 : entries.length - 1)
|
||||||
: enabledEntries[0]?.entry
|
if (action === 'navigateRight') return Math.min(lastChoiceIndex, bounded + 1)
|
||||||
if (!active) return bounded
|
if (action === 'navigateLeft') return Math.max(0, bounded - 1)
|
||||||
const candidates = enabledEntries.filter(({ entry }) => {
|
if (action === 'navigateDown') return continueIndex >= 0 ? continueIndex : bounded
|
||||||
if (entry === active) return false
|
if (action === 'navigateUp' && active?.kind === 'upgradeContinue') return lastChoiceIndex
|
||||||
if (action === 'navigateLeft') return entry.row === active.row && entry.column < active.column
|
if (action === 'navigateUp' && active?.kind === 'upgradeLeave') return lastChoiceIndex
|
||||||
if (action === 'navigateRight') return entry.row === active.row && entry.column > active.column
|
return bounded
|
||||||
if (action === 'navigateUp') return entry.row < active.row
|
|
||||||
return entry.row > active.row
|
|
||||||
})
|
|
||||||
if (candidates.length === 0) return entries.findIndex((entry) => entry === active)
|
|
||||||
candidates.sort((a, b) => {
|
|
||||||
const aPrimary = Math.abs(a.entry.row - active.row) + Math.abs(a.entry.column - active.column)
|
|
||||||
const bPrimary = Math.abs(b.entry.row - active.row) + Math.abs(b.entry.column - active.column)
|
|
||||||
return aPrimary - bPrimary || a.index - b.index
|
|
||||||
})
|
|
||||||
return candidates[0]?.index ?? bounded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveSpatialSelection(actions: Iwt2NavAction[], current: number, action: string) {
|
function moveSpatialSelection(actions: Iwt2NavAction[], current: number, action: string) {
|
||||||
|
|||||||
@@ -56,9 +56,13 @@ const INITIAL_PARTY: InitialPartyMember[] = [
|
|||||||
{ id: 'warrior', classId: 'warrior', aiRole: 'melee', x: 515, y: 310, preferredOffset: { x: -18, y: 64 }, decisionOffset: 0.24 },
|
{ id: 'warrior', classId: 'warrior', aiRole: 'melee', x: 515, y: 310, preferredOffset: { x: -18, y: 64 }, decisionOffset: 0.24 },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome', bossIds?: Iwt2BossId[]): Iwt2ArenaState {
|
export function createInitialIwt2ArenaState(
|
||||||
|
bossId: Iwt2BossId = 'bulldrome',
|
||||||
|
bossIds?: Iwt2BossId[],
|
||||||
|
bossHealthScale = 1,
|
||||||
|
): Iwt2ArenaState {
|
||||||
const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId)
|
const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId)
|
||||||
const bosses = initialBossIds.map((id, index) => createBossEntity(id, index))
|
const bosses = initialBossIds.map((id, index) => createBossEntity(id, index, bossHealthScale))
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
time: 0,
|
time: 0,
|
||||||
@@ -84,9 +88,10 @@ function chooseInitialBossIds(primaryBossId: Iwt2BossId): Iwt2BossId[] {
|
|||||||
return [primaryBossId, random]
|
return [primaryBossId, random]
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBossEntity(bossId: Iwt2BossId, index: number): Iwt2BossEntityState {
|
function createBossEntity(bossId: Iwt2BossId, index: number, healthScale: number): Iwt2BossEntityState {
|
||||||
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
||||||
const position = initialBossPosition(index)
|
const position = initialBossPosition(index)
|
||||||
|
const maxHealth = Math.max(1, Math.round(bossMetadata.maxHealth * Math.max(0.01, healthScale)))
|
||||||
return {
|
return {
|
||||||
id: bossId,
|
id: bossId,
|
||||||
kind: 'boss',
|
kind: 'boss',
|
||||||
@@ -95,8 +100,8 @@ function createBossEntity(bossId: Iwt2BossId, index: number): Iwt2BossEntityStat
|
|||||||
velocity: { x: 0, y: 0 },
|
velocity: { x: 0, y: 0 },
|
||||||
facing: { x: -1, y: 0 },
|
facing: { x: -1, y: 0 },
|
||||||
radius: bossMetadata.radius,
|
radius: bossMetadata.radius,
|
||||||
health: bossMetadata.maxHealth,
|
health: maxHealth,
|
||||||
maxHealth: bossMetadata.maxHealth,
|
maxHealth,
|
||||||
meleeCooldownRemaining: 0.6 + index * 0.25,
|
meleeCooldownRemaining: 0.6 + index * 0.25,
|
||||||
chargeCooldownRemaining: initialBossSpecialCooldown(bossId) + index * 0.7,
|
chargeCooldownRemaining: initialBossSpecialCooldown(bossId) + index * 0.7,
|
||||||
chargeCount: 0,
|
chargeCount: 0,
|
||||||
|
|||||||
@@ -54,8 +54,12 @@ export type Iwt2ArenaState = Iwt2CoreArenaState & {
|
|||||||
telegraphs: Iwt2ArenaTelegraph[]
|
telegraphs: Iwt2ArenaTelegraph[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInitialIwt2ArenaState(bossId?: Iwt2BossId, bossIds?: Iwt2BossId[]): Iwt2ArenaState {
|
export function createInitialIwt2ArenaState(
|
||||||
return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds))
|
bossId?: Iwt2BossId,
|
||||||
|
bossIds?: Iwt2BossId[],
|
||||||
|
bossHealthScale?: number,
|
||||||
|
): Iwt2ArenaState {
|
||||||
|
return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds, bossHealthScale))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
|
export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import {
|
|||||||
withFallbackFacing,
|
withFallbackFacing,
|
||||||
} from './vector'
|
} from './vector'
|
||||||
|
|
||||||
const CENTER_LEASH_BOSS_IDS = new Set(['great-jaggi', 'khezu'])
|
const NO_CENTER_LEASH_BOSS_IDS = new Set<string>()
|
||||||
const CENTER_LEASH_START_DISTANCE = 230
|
const CENTER_LEASH_START_DISTANCE = 230
|
||||||
const CENTER_LEASH_WALL_MARGIN = 96
|
const CENTER_LEASH_WALL_MARGIN = 96
|
||||||
const CENTER_LEASH_EXTRA_DISTANCE = 36
|
const CENTER_LEASH_EXTRA_DISTANCE = 36
|
||||||
const PARTY_ARRIVAL_RADIUS = 3.5
|
const PARTY_ARRIVAL_RADIUS = 3.5
|
||||||
|
const PARTY_SAFE_DESTINATION_PADDING = 34
|
||||||
|
|
||||||
export function tickPartyMember(
|
export function tickPartyMember(
|
||||||
member: Iwt2PartyEntityState,
|
member: Iwt2PartyEntityState,
|
||||||
@@ -128,24 +129,29 @@ function getPartyDesiredPosition(
|
|||||||
const anchor = attackTarget?.position ?? getPrimaryBoss(state).position
|
const anchor = attackTarget?.position ?? getPrimaryBoss(state).position
|
||||||
const centerLeash = getTankCenterLeashPosition(member, state)
|
const centerLeash = getTankCenterLeashPosition(member, state)
|
||||||
if (centerLeash) return centerLeash
|
if (centerLeash) return centerLeash
|
||||||
return {
|
return clampPartyDestination({
|
||||||
x: anchor.x + member.preferredOffset.x + drift.x,
|
x: anchor.x + member.preferredOffset.x + drift.x,
|
||||||
y: anchor.y + member.preferredOffset.y + drift.y,
|
y: anchor.y + member.preferredOffset.y + drift.y,
|
||||||
}
|
}, member, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampPartyDestination(
|
||||||
|
position: Iwt2Vec2,
|
||||||
|
member: Iwt2PartyEntityState,
|
||||||
|
state: Iwt2ArenaState,
|
||||||
|
): Iwt2Vec2 {
|
||||||
|
return clampVec2ToArena(position, member.radius + PARTY_SAFE_DESTINATION_PADDING, state.bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTankCenterLeashPosition(
|
function getTankCenterLeashPosition(
|
||||||
member: Iwt2PartyEntityState,
|
member: Iwt2PartyEntityState,
|
||||||
state: Iwt2ArenaState,
|
state: Iwt2ArenaState,
|
||||||
): Iwt2Vec2 | null {
|
): Iwt2Vec2 | null {
|
||||||
const boss = getPrimaryBoss(state)
|
if (member.aiRole !== 'tank') return null
|
||||||
if (member.aiRole !== 'tank' || !CENTER_LEASH_BOSS_IDS.has(boss.bossId)) return null
|
const boss = getCenterLeashBoss(member, state)
|
||||||
|
if (!boss) return null
|
||||||
|
|
||||||
const center = arenaCenter(state)
|
const center = arenaCenter(state)
|
||||||
const bossCenterDistance = distanceVec2(boss.position, center)
|
|
||||||
const nearWall = isBossNearWall(boss, state)
|
|
||||||
if (!nearWall && bossCenterDistance < CENTER_LEASH_START_DISTANCE) return null
|
|
||||||
|
|
||||||
const bossMetadata = IWT2_BOSS_METADATA[boss.bossId]
|
const bossMetadata = IWT2_BOSS_METADATA[boss.bossId]
|
||||||
const towardCenter = withFallbackFacing(subtractVec2(center, boss.position), {
|
const towardCenter = withFallbackFacing(subtractVec2(center, boss.position), {
|
||||||
x: boss.position.x < center.x ? 1 : -1,
|
x: boss.position.x < center.x ? 1 : -1,
|
||||||
@@ -159,6 +165,21 @@ function getTankCenterLeashPosition(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCenterLeashBoss(
|
||||||
|
member: Iwt2PartyEntityState,
|
||||||
|
state: Iwt2ArenaState,
|
||||||
|
): Iwt2ArenaState['boss'] | null {
|
||||||
|
const center = arenaCenter(state)
|
||||||
|
const candidates = state.bosses.filter((boss) => {
|
||||||
|
if (boss.health <= 0 || NO_CENTER_LEASH_BOSS_IDS.has(boss.bossId)) return false
|
||||||
|
return isBossNearWall(boss, state) || distanceVec2(boss.position, center) >= CENTER_LEASH_START_DISTANCE
|
||||||
|
})
|
||||||
|
if (candidates.length === 0) return null
|
||||||
|
return candidates.reduce((best, boss) => (
|
||||||
|
distanceVec2(member.position, boss.position) < distanceVec2(member.position, best.position) ? boss : best
|
||||||
|
), candidates[0])
|
||||||
|
}
|
||||||
|
|
||||||
function arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
|
function arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
|
||||||
return {
|
return {
|
||||||
x: state.bounds.width * 0.5,
|
x: state.bounds.width * 0.5,
|
||||||
|
|||||||
Reference in New Issue
Block a user