Android build v1.0.55

This commit is contained in:
Warren H
2026-06-21 20:38:55 -04:00
parent 787e2bbae9
commit 5449276521
5 changed files with 213 additions and 48 deletions
+139 -27
View File
@@ -28,6 +28,7 @@ import {
joinPvpQueue,
loadPvpMatch,
publishPvpMatchState,
requestPvpRematch,
randomCpuDifficulty,
recordCpuPvpLeaderboard,
recordPvpRoguelikeCheckpoint,
@@ -35,6 +36,7 @@ import {
type CpuDifficulty,
type PvpMatchSnapshot,
type PvpMatchSide,
type PvpRematchResponse,
type PvpContentType,
type PvpUpgradeChoicePayload,
} from '../pvpRoguelike'
@@ -469,6 +471,8 @@ export function PvPRoguelikeScreen({
const [cpuDifficulty, setCpuDifficulty] = useState<CpuDifficulty | null>(null)
const [liveMatch, setLiveMatch] = useState<LivePvpMatch | null>(null)
const [liveUpgradePending, setLiveUpgradePending] = useState(false)
const [rematchRequested, setRematchRequested] = useState(false)
const [rematchMessage, setRematchMessage] = useState('')
const [queueMessage, setQueueMessage] = useState('')
const [log, setLog] = useState<CombatLogEntry[]>([{ id: 1, text: 'Queueing opponent...', tone: 'system' }])
const [reward, setReward] = useState<DungeonReward | null>(null)
@@ -635,6 +639,80 @@ export function PvPRoguelikeScreen({
: null)
}, [opponentDebuffChoicesCatalog, selfBuffChoicesCatalog])
const startLiveMatch = useCallback((
match: PvpMatchSnapshot<SideState>,
side: PvpMatchSide,
message?: string,
) => {
const matchStartStage = match.startStage
const firstSegment = buildEncounterSegment(encounterPoolRef.current, matchStartStage, contentType)
const firstEncounter = firstSegment[0]
const basePlayer = starterSide(partyTemplate, maxResource)
const opponentSide: PvpMatchSide = side === 'a' ? 'b' : 'a'
const opponent = match.players[opponentSide]
const baseOpponent = starterSide(
cpuPartyTemplate.map((member) => ({
...member,
name: member.id === 'mira' ? opponent.characterName : member.name,
})),
maxResource,
)
basePlayer.enemyHealth = firstEncounter.maxHealth
baseOpponent.enemyHealth = firstEncounter.maxHealth
const nextLiveMatch: LivePvpMatch = {
id: match.id,
side,
opponentSide,
opponentName: opponent.characterName,
opponentClassName: opponent.className,
}
playerRef.current = basePlayer
cpuRef.current = baseOpponent
liveMatchRef.current = nextLiveMatch
nextLogId.current = 2
playerClearedEncounterRef.current = -1
queuedMatchRef.current = true
bossRewardClaimedRef.current = new Set()
setEncounters(firstSegment)
setEncounterIndex(0)
setCheckpointStage(matchStartStage)
setStartStage(matchStartStage)
setStage(matchStartStage)
setElapsedTicks(0)
setStatus('playing')
setPlayerSide(basePlayer)
setCpuSide(baseOpponent)
setSelectedTargetId(partyTemplate[0].id)
setPlayerBuffChoices([])
setPlayerDebuffChoices([])
setSelectedBuff(null)
setSelectedDebuff(null)
setUpgradeTimeLeft(UPGRADE_CHOICE_SECONDS)
upgradeChoiceEndsAtRef.current = 0
autoSubmittedUpgradeRef.current = false
setEncountersCleared(0)
setPaused(false)
setTargetGroup(0)
setReward(null)
setRunSummary(createEmptyPvpRunSummary())
setRewardError('')
setShowEndLog(false)
setFloatingTexts([])
setCpuDifficulty(null)
setLiveMatch(nextLiveMatch)
setLiveUpgradePending(false)
pendingLiveUpgradeRef.current = null
loggedOpponentDoneRef.current = false
recordedRunRef.current = false
rewardClaimedRef.current = false
cpuDefeatedRef.current = false
setRematchRequested(false)
setRematchMessage('')
const logText = message ?? `${opponent.characterName} found. Stage ${matchStartStage} begins.`
setQueueMessage(logText)
setLog([{ id: 1, text: logText, tone: 'system' }])
}, [contentType, cpuPartyTemplate, maxResource, partyTemplate, setSelectedTargetId])
const startMatch = useCallback((nextStartStage?: number) => {
const matchStartStage = nextStartStage ?? loadPvpRoguelikeCheckpoint(profile.character.id, contentType)
const firstSegment = buildEncounterSegment(encounterPoolRef.current, matchStartStage, contentType)
@@ -680,6 +758,8 @@ export function PvPRoguelikeScreen({
setLiveUpgradePending(false)
pendingLiveUpgradeRef.current = null
loggedOpponentDoneRef.current = false
setRematchRequested(false)
setRematchMessage('')
recordedRunRef.current = false
rewardClaimedRef.current = false
cpuDefeatedRef.current = false
@@ -709,29 +789,7 @@ export function PvPRoguelikeScreen({
if (cancelled) return
const opponentSide: PvpMatchSide = side === 'a' ? 'b' : 'a'
const opponent = match.players[opponentSide]
const nextLiveMatch = {
id: match.id,
side,
opponentSide,
opponentName: opponent.characterName,
opponentClassName: opponent.className,
}
liveMatchRef.current = nextLiveMatch
setLiveMatch(nextLiveMatch)
setCpuDifficulty(null)
const opponentBase = starterSide(
cpuPartyTemplate.map((member) => ({
...member,
name: member.id === 'mira' ? opponent.characterName : member.name,
})),
maxResource,
)
opponentBase.enemyHealth = firstEncounter.maxHealth
cpuRef.current = opponentBase
setCpuSide(opponentBase)
setQueueMessage(`${opponent.characterName} found. Match begins.`)
setLog([{ id: 1, text: `${opponent.characterName} found. Stage ${matchStartStage} begins.`, tone: 'system' }])
setStatus('playing')
startLiveMatch(match, side, `${opponent.characterName} found. Stage ${match.startStage} begins.`)
}
const fallbackTimer = window.setTimeout(() => {
if (cancelled || liveMatchRef.current) return
@@ -781,7 +839,7 @@ export function PvPRoguelikeScreen({
if (pollTimer) window.clearTimeout(pollTimer)
if (ticketId && !liveMatchRef.current) cancelPvpQueue(ticketId).catch(() => undefined)
}
}, [addLog, contentType, cpuPartyTemplate, gameMode, maxResource, partyTemplate, profile.character.id, setSelectedTargetId])
}, [addLog, contentType, cpuPartyTemplate, gameMode, maxResource, partyTemplate, profile.character.id, setSelectedTargetId, startLiveMatch])
useEffect(() => startMatch(), [startMatch])
@@ -807,10 +865,13 @@ export function PvPRoguelikeScreen({
setCpuSide(opponentState)
}
const opponentStatus = snapshot.statuses[liveMatch.opponentSide]
if (opponentStatus === 'lost' && !loggedOpponentDoneRef.current) {
const opponentAlive = snapshot.progress[liveMatch.opponentSide]?.alive
if ((opponentStatus === 'lost' || opponentAlive === false) && !loggedOpponentDoneRef.current && status !== 'won' && status !== 'lost') {
loggedOpponentDoneRef.current = true
cpuDefeatedRef.current = true
addLog(`${liveMatch.opponentName} fell. Finish the boss for XP.`, 'loot')
finishRoguelikeRun()
setStatus('won')
addLog(`${liveMatch.opponentName} fell. Match complete.`, 'loot')
}
if (opponentStatus === 'won' && status !== 'won' && status !== 'lost') {
finishRoguelikeRun()
@@ -1188,7 +1249,10 @@ export function PvPRoguelikeScreen({
}
if (!liveMatch && !nextCpuAlive && !cpuDefeatedRef.current) {
cpuDefeatedRef.current = true
addLog(`CPU ${cpuDifficulty ?? 1} fell. Finish the boss for XP.`, 'loot')
finishRoguelikeRun()
setStatus('won')
addLog(`CPU ${cpuDifficulty ?? 1} fell. Match complete.`, 'loot')
return
}
if (nextPlayer.enemyHealth <= 0) {
if (encounter.isBoss && cpuDefeatedRef.current) {
@@ -1218,6 +1282,46 @@ export function PvPRoguelikeScreen({
})
}, [contentType, cpuDifficulty, finalEncountersCleared, profile.character.className, profile.character.name, status])
const handleRematch = useCallback(() => {
if (!liveMatch || rematchRequested) return
let cancelled = false
let attempts = 0
setRematchRequested(true)
setRematchMessage(`Waiting for ${liveMatch.opponentName} to rematch...`)
const handleResponse = (result: PvpRematchResponse<SideState>) => {
if (cancelled) return
if (result.status === 'matched' && result.match && result.side) {
startLiveMatch(result.match, result.side, `Rematch against ${liveMatch.opponentName} begins.`)
return
}
attempts += 1
if (attempts >= 180) {
setRematchRequested(false)
setRematchMessage('Rematch expired.')
return
}
window.setTimeout(pollRematch, 700)
}
const pollRematch = () => {
requestPvpRematch<SideState>(liveMatch.id)
.then(handleResponse)
.catch((reason: unknown) => {
if (cancelled) return
attempts += 1
if (attempts >= 10) {
setRematchRequested(false)
setRematchMessage(reason instanceof Error ? reason.message : 'Unable to request rematch.')
return
}
window.setTimeout(pollRematch, 900)
})
}
pollRematch()
return () => {
cancelled = true
}
}, [liveMatch, rematchRequested, startLiveMatch])
useEffect(() => {
if (status !== 'upgrade-choice') return
window.requestAnimationFrame(() => focusFirstControl())
@@ -1898,6 +2002,14 @@ export function PvPRoguelikeScreen({
)}
</>
)}
{liveMatch && (
<>
<button disabled={rematchRequested} onClick={handleRematch} type="button">
{rematchRequested ? 'Waiting for Rematch' : 'Rematch'}
</button>
{rematchMessage && <p>{rematchMessage}</p>}
</>
)}
<button onClick={() => startMatch()} type="button">Queue Next Match</button>
<button className="secondary-result-button" onClick={onExit} type="button">Back to Roguelike</button>
</div>