Android build v1.0.57
This commit is contained in:
@@ -42,7 +42,8 @@ import {
|
||||
} from '../pvpRoguelike'
|
||||
|
||||
const TICK_MS = 700
|
||||
const UPGRADE_CHOICE_SECONDS = 10
|
||||
const ROUND_START_SECONDS = 3
|
||||
const UPGRADE_CHOICE_SECONDS = 15
|
||||
|
||||
type BossMechanic =
|
||||
| 'party-pulse'
|
||||
@@ -61,6 +62,7 @@ type DraftSlotKey = Exclude<SlotKey, '6'>
|
||||
type AbilityLabelMode = 'ability' | 'slot'
|
||||
|
||||
type SelfBuffId =
|
||||
| 'revive-party-members'
|
||||
| `slot${DraftSlotKey}-extra-target`
|
||||
| `slot${DraftSlotKey}-cost-down`
|
||||
| `slot${DraftSlotKey}-cooldown-down`
|
||||
@@ -112,6 +114,12 @@ type LivePvpMatch = {
|
||||
opponentClassName: string
|
||||
}
|
||||
|
||||
const REVIVE_PARTY_CHOICE: Choice<SelfBuffId> = {
|
||||
id: 'revive-party-members',
|
||||
name: 'Revive Party Members',
|
||||
description: 'Revive fallen party members before the next fight.',
|
||||
}
|
||||
|
||||
const BOSS_MECHANICS: BossMechanic[] = [
|
||||
'party-pulse',
|
||||
'searing-mark',
|
||||
@@ -322,7 +330,32 @@ function starterSide(partyTemplate: PartyMember[], maxResource: number): SideSta
|
||||
}
|
||||
}
|
||||
|
||||
function hasDeadPartyMembers(side: SideState) {
|
||||
return side.party.some((member) => member.health <= 0)
|
||||
}
|
||||
|
||||
function recoverPartyForNextEncounter(party: PartyMember[], reviveDead: boolean) {
|
||||
return party.map((member) => ({
|
||||
...member,
|
||||
health: member.health <= 0
|
||||
? (reviveDead ? Math.max(1, Math.round(member.maxHealth * 0.3)) : 0)
|
||||
: clamp(member.health + Math.round(member.maxHealth * 0.3), 0, member.maxHealth),
|
||||
debuff: undefined,
|
||||
debuffTicks: undefined,
|
||||
poisonStacks: undefined,
|
||||
maxHealthPenaltyTicks: undefined,
|
||||
healingReductionTicks: undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
function removeRandomDebuff(debuffs: OpponentDebuffId[]) {
|
||||
if (debuffs.length === 0) return debuffs
|
||||
const removedIndex = Math.floor(Math.random() * debuffs.length)
|
||||
return debuffs.filter((_, index) => index !== removedIndex)
|
||||
}
|
||||
|
||||
function scoreSelfBuff(buff: Choice<SelfBuffId>, spells: Spell[]) {
|
||||
if (buff.id === 'revive-party-members') return 10
|
||||
const slot = buff.id.match(/slot([1-6])/i)?.[1] as SlotKey | undefined
|
||||
const spell = spells.find((candidate) => candidate.key === slot)
|
||||
if (!spell) return 5
|
||||
@@ -416,7 +449,7 @@ export function PvPRoguelikeScreen({
|
||||
})),
|
||||
[contentType],
|
||||
)
|
||||
const [status, setStatus] = useState<'queueing' | 'playing' | 'upgrade-choice' | 'won' | 'lost'>('queueing')
|
||||
const [status, setStatus] = useState<'queueing' | 'round-countdown' | 'playing' | 'upgrade-choice' | 'won' | 'lost'>('queueing')
|
||||
const [stage, setStage] = useState(startStage)
|
||||
const [encounters, setEncounters] = useState<PvpEncounter[]>(() => buildEncounterSegment(encounterPool, startStage, contentType))
|
||||
const [encounterIndex, setEncounterIndex] = useState(0)
|
||||
@@ -442,6 +475,7 @@ export function PvPRoguelikeScreen({
|
||||
const [playerDebuffChoices, setPlayerDebuffChoices] = useState<Array<Choice<OpponentDebuffId>>>([])
|
||||
const [selectedBuff, setSelectedBuff] = useState<Choice<SelfBuffId> | null>(null)
|
||||
const [selectedDebuff, setSelectedDebuff] = useState<Choice<OpponentDebuffId> | null>(null)
|
||||
const [roundCountdown, setRoundCountdown] = useState(ROUND_START_SECONDS)
|
||||
const [upgradeTimeLeft, setUpgradeTimeLeft] = useState(UPGRADE_CHOICE_SECONDS)
|
||||
const [encountersCleared, setEncountersCleared] = useState(0)
|
||||
const [paused, setPaused] = useState(false)
|
||||
@@ -456,6 +490,7 @@ export function PvPRoguelikeScreen({
|
||||
const queuedMatchRef = useRef(false)
|
||||
const upgradeChoiceEndsAtRef = useRef(0)
|
||||
const autoSubmittedUpgradeRef = useRef(false)
|
||||
const roundCountdownTimerRef = useRef<number | null>(null)
|
||||
const liveMatchRef = useRef<LivePvpMatch | null>(null)
|
||||
const loggedOpponentDoneRef = useRef(false)
|
||||
const pendingLiveUpgradeRef = useRef<{
|
||||
@@ -518,6 +553,29 @@ export function PvPRoguelikeScreen({
|
||||
}, 900)
|
||||
}, [])
|
||||
|
||||
const clearRoundCountdown = useCallback(() => {
|
||||
if (roundCountdownTimerRef.current === null) return
|
||||
window.clearInterval(roundCountdownTimerRef.current)
|
||||
roundCountdownTimerRef.current = null
|
||||
}, [])
|
||||
|
||||
const beginRoundCountdown = useCallback((message?: string) => {
|
||||
clearRoundCountdown()
|
||||
setRoundCountdown(ROUND_START_SECONDS)
|
||||
setStatus('round-countdown')
|
||||
if (message) addLog(message, 'system')
|
||||
const startedAt = Date.now()
|
||||
roundCountdownTimerRef.current = window.setInterval(() => {
|
||||
const remaining = Math.max(0, ROUND_START_SECONDS - (Date.now() - startedAt) / 1000)
|
||||
setRoundCountdown(remaining)
|
||||
if (remaining > 0) return
|
||||
clearRoundCountdown()
|
||||
setStatus((current) => current === 'round-countdown' ? 'playing' : current)
|
||||
}, 100)
|
||||
}, [addLog, clearRoundCountdown])
|
||||
|
||||
useEffect(() => () => clearRoundCountdown(), [clearRoundCountdown])
|
||||
|
||||
useEffect(() => {
|
||||
if (queuedMatchRef.current) return
|
||||
const loadedCheckpoint = loadPvpRoguelikeCheckpoint(profile.character.id, contentType)
|
||||
@@ -589,13 +647,15 @@ export function PvPRoguelikeScreen({
|
||||
|
||||
useEffect(() => {
|
||||
setPlayerBuffChoices((current) => current
|
||||
.map((choice) => selfBuffChoicesCatalog.find((candidate) => candidate.id === choice.id))
|
||||
.map((choice) => choice.id === REVIVE_PARTY_CHOICE.id
|
||||
? REVIVE_PARTY_CHOICE
|
||||
: selfBuffChoicesCatalog.find((candidate) => candidate.id === choice.id))
|
||||
.filter((choice): choice is Choice<SelfBuffId> => Boolean(choice)))
|
||||
setPlayerDebuffChoices((current) => current
|
||||
.map((choice) => opponentDebuffChoicesCatalog.find((candidate) => candidate.id === choice.id))
|
||||
.filter((choice): choice is Choice<OpponentDebuffId> => Boolean(choice)))
|
||||
setSelectedBuff((current) => current
|
||||
? selfBuffChoicesCatalog.find((candidate) => candidate.id === current.id) ?? current
|
||||
? (current.id === REVIVE_PARTY_CHOICE.id ? REVIVE_PARTY_CHOICE : selfBuffChoicesCatalog.find((candidate) => candidate.id === current.id) ?? current)
|
||||
: null)
|
||||
setSelectedDebuff((current) => current
|
||||
? opponentDebuffChoicesCatalog.find((candidate) => candidate.id === current.id) ?? current
|
||||
@@ -642,7 +702,6 @@ export function PvPRoguelikeScreen({
|
||||
setStartStage(matchStartStage)
|
||||
setStage(matchStartStage)
|
||||
setElapsedTicks(0)
|
||||
setStatus('playing')
|
||||
setPlayerSide(basePlayer)
|
||||
setCpuSide(baseOpponent)
|
||||
setSelectedTargetId(partyTemplate[0].id)
|
||||
@@ -674,9 +733,11 @@ export function PvPRoguelikeScreen({
|
||||
const logText = message ?? `${opponent.characterName} found. Stage ${matchStartStage} begins.`
|
||||
setQueueMessage(logText)
|
||||
setLog([{ id: 1, text: logText, tone: 'system' }])
|
||||
}, [contentType, cpuPartyTemplate, maxResource, partyTemplate, setSelectedTargetId])
|
||||
beginRoundCountdown()
|
||||
}, [beginRoundCountdown, contentType, cpuPartyTemplate, maxResource, partyTemplate, setSelectedTargetId])
|
||||
|
||||
const startMatch = useCallback((nextStartStage?: number) => {
|
||||
clearRoundCountdown()
|
||||
const matchStartStage = nextStartStage ?? loadPvpRoguelikeCheckpoint(profile.character.id, contentType)
|
||||
const firstSegment = buildEncounterSegment(encounterPoolRef.current, matchStartStage, contentType)
|
||||
const firstEncounter = firstSegment[0]
|
||||
@@ -732,8 +793,7 @@ export function PvPRoguelikeScreen({
|
||||
setCpuDifficulty(randomCpu)
|
||||
setQueueMessage(message)
|
||||
setLog([{ id: 1, text: message, tone: 'system' }])
|
||||
setStatus('playing')
|
||||
addLog(`Stage ${matchStartStage} begins against CPU ${randomCpu}.`, 'system')
|
||||
beginRoundCountdown(`Stage ${matchStartStage} begins against CPU ${randomCpu}.`)
|
||||
}
|
||||
if (gameMode === 'offline') {
|
||||
const randomCpu = randomCpuDifficulty()
|
||||
@@ -802,7 +862,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, startLiveMatch])
|
||||
}, [beginRoundCountdown, clearRoundCountdown, contentType, cpuPartyTemplate, gameMode, maxResource, partyTemplate, profile.character.id, setSelectedTargetId, startLiveMatch])
|
||||
|
||||
useEffect(() => startMatch(), [startMatch])
|
||||
|
||||
@@ -812,7 +872,7 @@ export function PvPRoguelikeScreen({
|
||||
const syncMatch = () => {
|
||||
publishPvpMatchState<SideState>(liveMatch.id, {
|
||||
state: playerRef.current,
|
||||
status: status === 'upgrade-choice' ? 'upgrade-choice' : status,
|
||||
status: status === 'upgrade-choice' ? 'upgrade-choice' : status === 'round-countdown' ? 'playing' : status,
|
||||
stage,
|
||||
encounterIndex,
|
||||
encountersCleared,
|
||||
@@ -1154,10 +1214,11 @@ export function PvPRoguelikeScreen({
|
||||
const beginUpgradePhase = useCallback(() => {
|
||||
upgradeChoiceEndsAtRef.current = Date.now() + UPGRADE_CHOICE_SECONDS * 1000
|
||||
autoSubmittedUpgradeRef.current = false
|
||||
const playerNeedsRevive = hasDeadPartyMembers(playerRef.current)
|
||||
setUpgradeTimeLeft(UPGRADE_CHOICE_SECONDS)
|
||||
setPlayerBuffChoices(chooseRandom(selfBuffChoicesCatalog, 3))
|
||||
setPlayerBuffChoices(playerNeedsRevive ? [REVIVE_PARTY_CHOICE] : chooseRandom(selfBuffChoicesCatalog, 3))
|
||||
setPlayerDebuffChoices(chooseRandom(opponentDebuffChoicesCatalog, 3))
|
||||
setSelectedBuff(null)
|
||||
setSelectedBuff(playerNeedsRevive ? REVIVE_PARTY_CHOICE : null)
|
||||
setSelectedDebuff(null)
|
||||
setStatus('upgrade-choice')
|
||||
}, [opponentDebuffChoicesCatalog, selfBuffChoicesCatalog])
|
||||
@@ -1297,11 +1358,16 @@ export function PvPRoguelikeScreen({
|
||||
const applyLiveUpgrade = (opponentChoice: PvpUpgradeChoicePayload) => {
|
||||
let nextPlayer = {
|
||||
...playerRef.current,
|
||||
buffs: [...playerRef.current.buffs, submittedBuff.id],
|
||||
buffs: submittedBuff.id === REVIVE_PARTY_CHOICE.id
|
||||
? playerRef.current.buffs
|
||||
: [...playerRef.current.buffs, submittedBuff.id],
|
||||
}
|
||||
if (opponentDebuffChoicesCatalog.some((choice) => choice.id === opponentChoice.debuffId)) {
|
||||
nextPlayer = { ...nextPlayer, debuffs: [...nextPlayer.debuffs, opponentChoice.debuffId as OpponentDebuffId] }
|
||||
}
|
||||
if (submittedBuff.id === REVIVE_PARTY_CHOICE.id) {
|
||||
nextPlayer = { ...nextPlayer, debuffs: removeRandomDebuff(nextPlayer.debuffs) }
|
||||
}
|
||||
|
||||
const clearedBoss = encounter.isBoss
|
||||
if (clearedBoss && cpuDefeatedRef.current) {
|
||||
@@ -1323,15 +1389,7 @@ export function PvPRoguelikeScreen({
|
||||
}
|
||||
nextPlayer = {
|
||||
...nextPlayer,
|
||||
party: nextPlayer.party.map((member) => ({
|
||||
...member,
|
||||
health: member.health <= 0 ? 0 : clamp(member.health + Math.round(member.maxHealth * 0.3), 0, member.maxHealth),
|
||||
debuff: undefined,
|
||||
debuffTicks: undefined,
|
||||
poisonStacks: undefined,
|
||||
maxHealthPenaltyTicks: undefined,
|
||||
healingReductionTicks: undefined,
|
||||
})),
|
||||
party: recoverPartyForNextEncounter(nextPlayer.party, submittedBuff.id === REVIVE_PARTY_CHOICE.id),
|
||||
resource: clamp(nextPlayer.resource + Math.round(maxResource * 0.25), 0, maxResource),
|
||||
cooldowns: {},
|
||||
enemyHealth: nextEncounter.maxHealth,
|
||||
@@ -1346,7 +1404,7 @@ export function PvPRoguelikeScreen({
|
||||
setElapsedTicks(0)
|
||||
setLiveUpgradePending(false)
|
||||
pendingLiveUpgradeRef.current = null
|
||||
setStatus('playing')
|
||||
beginRoundCountdown()
|
||||
const opponentDebuff = opponentDebuffChoicesCatalog.find((choice) => choice.id === opponentChoice.debuffId)
|
||||
addLog(
|
||||
`You chose ${submittedBuff.name} and ${submittedDebuff.name}. ${liveMatch.opponentName} chose ${opponentDebuff?.name ?? 'an opponent debuff'}.`,
|
||||
@@ -1392,23 +1450,35 @@ export function PvPRoguelikeScreen({
|
||||
return
|
||||
}
|
||||
if (!cpuDifficulty) return
|
||||
const cpuBuffChoices = chooseRandom(selfBuffChoicesCatalog, 3)
|
||||
const cpuBuffChoices = hasDeadPartyMembers(cpuRef.current)
|
||||
? [REVIVE_PARTY_CHOICE]
|
||||
: chooseRandom(selfBuffChoicesCatalog, 3)
|
||||
const cpuDebuffChoices = chooseRandom(opponentDebuffChoicesCatalog, 3)
|
||||
const cpuBuff = selectCpuChoice(cpuBuffChoices, cpuDifficulty, (choice) => scoreSelfBuff(choice, starterSpells))
|
||||
const cpuDebuff = selectCpuChoice(cpuDebuffChoices, cpuDifficulty, (choice) => scoreDebuff(choice, playerRef.current.buffs.length))
|
||||
|
||||
let nextPlayer = {
|
||||
...playerRef.current,
|
||||
buffs: [...playerRef.current.buffs, chosenBuff.id],
|
||||
buffs: chosenBuff.id === REVIVE_PARTY_CHOICE.id
|
||||
? playerRef.current.buffs
|
||||
: [...playerRef.current.buffs, chosenBuff.id],
|
||||
}
|
||||
let nextCpu = {
|
||||
...cpuRef.current,
|
||||
buffs: [...cpuRef.current.buffs, cpuBuff.id],
|
||||
buffs: cpuBuff.id === REVIVE_PARTY_CHOICE.id
|
||||
? cpuRef.current.buffs
|
||||
: [...cpuRef.current.buffs, cpuBuff.id],
|
||||
}
|
||||
|
||||
nextCpu = { ...nextCpu, debuffs: [...nextCpu.debuffs, chosenDebuff.id] }
|
||||
|
||||
nextPlayer = { ...nextPlayer, debuffs: [...nextPlayer.debuffs, cpuDebuff.id] }
|
||||
if (chosenBuff.id === REVIVE_PARTY_CHOICE.id) {
|
||||
nextPlayer = { ...nextPlayer, debuffs: removeRandomDebuff(nextPlayer.debuffs) }
|
||||
}
|
||||
if (cpuBuff.id === REVIVE_PARTY_CHOICE.id) {
|
||||
nextCpu = { ...nextCpu, debuffs: removeRandomDebuff(nextCpu.debuffs) }
|
||||
}
|
||||
|
||||
const clearedBoss = encounter.isBoss
|
||||
if (clearedBoss && cpuDefeatedRef.current) {
|
||||
@@ -1429,30 +1499,14 @@ export function PvPRoguelikeScreen({
|
||||
|
||||
nextPlayer = {
|
||||
...nextPlayer,
|
||||
party: nextPlayer.party.map((member) => ({
|
||||
...member,
|
||||
health: member.health <= 0 ? 0 : clamp(member.health + Math.round(member.maxHealth * 0.3), 0, member.maxHealth),
|
||||
debuff: undefined,
|
||||
debuffTicks: undefined,
|
||||
poisonStacks: undefined,
|
||||
maxHealthPenaltyTicks: undefined,
|
||||
healingReductionTicks: undefined,
|
||||
})),
|
||||
party: recoverPartyForNextEncounter(nextPlayer.party, chosenBuff.id === REVIVE_PARTY_CHOICE.id),
|
||||
resource: clamp(nextPlayer.resource + Math.round(maxResource * 0.25), 0, maxResource),
|
||||
cooldowns: {},
|
||||
enemyHealth: nextEncounter.maxHealth,
|
||||
}
|
||||
nextCpu = {
|
||||
...nextCpu,
|
||||
party: nextCpu.party.map((member) => ({
|
||||
...member,
|
||||
health: member.health <= 0 ? 0 : clamp(member.health + Math.round(member.maxHealth * 0.3), 0, member.maxHealth),
|
||||
debuff: undefined,
|
||||
debuffTicks: undefined,
|
||||
poisonStacks: undefined,
|
||||
maxHealthPenaltyTicks: undefined,
|
||||
healingReductionTicks: undefined,
|
||||
})),
|
||||
party: recoverPartyForNextEncounter(nextCpu.party, cpuBuff.id === REVIVE_PARTY_CHOICE.id),
|
||||
resource: clamp(nextCpu.resource + Math.round(maxResource * 0.25), 0, maxResource),
|
||||
cooldowns: {},
|
||||
enemyHealth: nextEncounter.maxHealth,
|
||||
@@ -1468,9 +1522,9 @@ export function PvPRoguelikeScreen({
|
||||
playerRef.current = nextPlayer
|
||||
cpuRef.current = nextCpu
|
||||
setElapsedTicks(0)
|
||||
setStatus('playing')
|
||||
beginRoundCountdown()
|
||||
addLog(`You chose ${chosenBuff.name} and ${chosenDebuff.name}. CPU ${cpuDifficulty} chose ${cpuBuff.name} and ${cpuDebuff.name}.`, 'system')
|
||||
}, [addLog, contentType, cpuDifficulty, encounter, encounterIndex, encounterPool, encounters, finishRoguelikeRun, liveMatch, maxResource, opponentDebuffChoicesCatalog, selectedBuff, selectedDebuff, selfBuffChoicesCatalog, stage, starterSpells])
|
||||
}, [addLog, beginRoundCountdown, contentType, cpuDifficulty, encounter, encounterIndex, encounterPool, encounters, finishRoguelikeRun, liveMatch, maxResource, opponentDebuffChoicesCatalog, selectedBuff, selectedDebuff, selfBuffChoicesCatalog, stage, starterSpells])
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'upgrade-choice' || liveUpgradePending) return
|
||||
@@ -1574,7 +1628,7 @@ export function PvPRoguelikeScreen({
|
||||
partySize: playerSide.party.length,
|
||||
selectedId,
|
||||
log,
|
||||
status: status === 'queueing' ? 'playing' : status,
|
||||
status: status === 'queueing' || status === 'round-countdown' ? 'playing' : status,
|
||||
resource: playerSide.resource,
|
||||
maxResource,
|
||||
resourceName: gameClass.resourceName,
|
||||
@@ -1802,6 +1856,15 @@ export function PvPRoguelikeScreen({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'round-countdown' && (
|
||||
<div className="pvp-round-countdown">
|
||||
<div>
|
||||
<p className="eyebrow">Round Starts</p>
|
||||
<h2>{Math.max(1, Math.ceil(roundCountdown))}</h2>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'upgrade-choice' && (
|
||||
<div className="result-screen">
|
||||
<div className="pvp-upgrade-dialog">
|
||||
@@ -1814,7 +1877,7 @@ export function PvPRoguelikeScreen({
|
||||
</div>
|
||||
<div className="pvp-choice-columns">
|
||||
<div>
|
||||
<strong>Self Buff</strong>
|
||||
<strong>{playerBuffChoices.length === 1 && playerBuffChoices[0]?.id === REVIVE_PARTY_CHOICE.id ? 'Recovery' : 'Self Buff'}</strong>
|
||||
<div className="upgrade-choice-grid">
|
||||
{playerBuffChoices.map((choice) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user