Files
i-want-to-heal/src/combat/pvpRoguelikeLifecycle.ts
T
2026-06-29 22:35:49 -04:00

154 lines
3.9 KiB
TypeScript

export type PvpRoguelikeStatus = 'queueing' | 'round-countdown' | 'playing' | 'upgrade-choice' | 'won' | 'lost'
export type PvpRoguelikeLogTone = 'system' | 'heal' | 'danger' | 'loot'
export type PvpRoguelikeCombatOutcome =
| {
type: 'none'
}
| {
type: 'lost'
status: 'lost'
log: { text: string; tone: PvpRoguelikeLogTone }
}
| {
type: 'won'
status: 'won'
markOpponentDefeated: boolean
log: { text: string; tone: PvpRoguelikeLogTone }
}
| {
type: 'upgrade-choice'
status: 'upgrade-choice'
log: { text: string; tone: PvpRoguelikeLogTone }
}
export function resolvePvpRoguelikeCombatOutcome({
playerAlive,
opponentAlive,
playerCleared,
encounterIsBoss,
encounterName,
opponentDefeated,
liveMatchActive,
opponentLabel,
}: {
playerAlive: boolean
opponentAlive: boolean
playerCleared: boolean
encounterIsBoss: boolean
encounterName: string
opponentDefeated: boolean
liveMatchActive: boolean
opponentLabel: string
}): PvpRoguelikeCombatOutcome {
if (!playerAlive) {
return {
type: 'lost',
status: 'lost',
log: { text: 'Your party fell first.', tone: 'danger' },
}
}
if (!liveMatchActive && !opponentAlive && !opponentDefeated) {
return {
type: 'won',
status: 'won',
markOpponentDefeated: true,
log: { text: `${opponentLabel} fell. Match complete.`, tone: 'loot' },
}
}
if (!playerCleared) return { type: 'none' }
if (encounterIsBoss && opponentDefeated) {
return {
type: 'won',
status: 'won',
markOpponentDefeated: false,
log: { text: `${opponentLabel} defeated. Match complete.`, tone: 'loot' },
}
}
return {
type: 'upgrade-choice',
status: 'upgrade-choice',
log: { text: `${encounterName} cleared. Choose your next edge.`, tone: 'loot' },
}
}
export function resolvePvpRoguelikeLiveSnapshot({
currentStatus,
opponentStatus,
opponentAlive,
alreadyLoggedOpponentDone,
opponentName,
}: {
currentStatus: PvpRoguelikeStatus
opponentStatus?: string
opponentAlive?: boolean
alreadyLoggedOpponentDone: boolean
opponentName: string
}) {
if (currentStatus === 'won' || currentStatus === 'lost') return { type: 'none' as const }
if ((opponentStatus === 'lost' || opponentAlive === false) && !alreadyLoggedOpponentDone) {
return {
type: 'won' as const,
status: 'won' as const,
markOpponentDefeated: true,
log: { text: `${opponentName} fell. Match complete.`, tone: 'loot' as const },
}
}
if (opponentStatus === 'won') {
return {
type: 'lost' as const,
status: 'lost' as const,
log: { text: `${opponentName} finished first.`, tone: 'danger' as const },
}
}
return { type: 'none' as const }
}
export function resolvePvpRoguelikeUpgradeCompletion({
clearedBoss,
opponentDefeated,
opponentLabel,
}: {
clearedBoss: boolean
opponentDefeated: boolean
opponentLabel: string
}) {
if (!clearedBoss || !opponentDefeated) return { type: 'none' as const }
return {
type: 'won' as const,
status: 'won' as const,
log: { text: `${opponentLabel} defeated. Match complete.`, tone: 'loot' as const },
}
}
export function nextPvpRoguelikeStage<TEncounter>({
clearedBoss,
stage,
encounterIndex,
encounters,
nextSegment,
}: {
clearedBoss: boolean
stage: number
encounterIndex: number
encounters: readonly TEncounter[]
nextSegment: readonly TEncounter[]
}) {
const nextStage = clearedBoss ? stage + 1 : stage
const nextEncounter = clearedBoss ? nextSegment[0] : encounters[encounterIndex + 1]
if (!nextEncounter) {
return {
type: 'complete' as const,
status: 'won' as const,
log: { text: 'No further encounters remain.', tone: 'loot' as const },
}
}
return {
type: 'next' as const,
nextStage,
nextEncounter,
nextEncounterIndex: encounterIndex + 1,
appendSegment: clearedBoss,
}
}