Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4703017832 |
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 76
|
versionCode 77
|
||||||
versionName "1.0.57"
|
versionName "1.0.58"
|
||||||
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.
|
||||||
|
|||||||
+40
-3
@@ -2309,7 +2309,13 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
|
|||||||
? 'pvp-boss-quarter-level'
|
? 'pvp-boss-quarter-level'
|
||||||
: runMetrics?.experienceMode === 'pvp-fight-twelfth-level'
|
: runMetrics?.experienceMode === 'pvp-fight-twelfth-level'
|
||||||
? 'pvp-fight-twelfth-level'
|
? 'pvp-fight-twelfth-level'
|
||||||
: 'default'
|
: runMetrics?.experienceMode === 'pvp-stadium-round-win-quarter-level'
|
||||||
|
? 'pvp-stadium-round-win-quarter-level'
|
||||||
|
: runMetrics?.experienceMode === 'pvp-stadium-round-loss-tenth-level'
|
||||||
|
? 'pvp-stadium-round-loss-tenth-level'
|
||||||
|
: runMetrics?.experienceMode === 'pvp-stadium-match-half-level'
|
||||||
|
? 'pvp-stadium-match-half-level'
|
||||||
|
: 'default'
|
||||||
const fightsCleared = Number(runMetrics?.fightsCleared ?? encountersCleared)
|
const fightsCleared = Number(runMetrics?.fightsCleared ?? encountersCleared)
|
||||||
const resourceSpent = Number(runMetrics?.resourceSpent)
|
const resourceSpent = Number(runMetrics?.resourceSpent)
|
||||||
const durationSeconds = Number(runMetrics?.durationSeconds)
|
const durationSeconds = Number(runMetrics?.durationSeconds)
|
||||||
@@ -2412,6 +2418,35 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
|
|||||||
WHERE experience_required <= ?
|
WHERE experience_required <= ?
|
||||||
`).get(newExperience).level
|
`).get(newExperience).level
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
experienceMode === 'pvp-stadium-round-win-quarter-level'
|
||||||
|
|| experienceMode === 'pvp-stadium-round-loss-tenth-level'
|
||||||
|
|| experienceMode === 'pvp-stadium-match-half-level'
|
||||||
|
) {
|
||||||
|
const currentLevelFloor = database.prepare(`
|
||||||
|
SELECT experience_required AS experienceRequired
|
||||||
|
FROM level_progression
|
||||||
|
WHERE level = ?
|
||||||
|
`).get(newLevel).experienceRequired
|
||||||
|
const nextLevelExperience = newLevel >= maxLevel
|
||||||
|
? maxExperience
|
||||||
|
: database.prepare(`
|
||||||
|
SELECT experience_required AS experienceRequired
|
||||||
|
FROM level_progression
|
||||||
|
WHERE level = ?
|
||||||
|
`).get(newLevel + 1).experienceRequired
|
||||||
|
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
|
||||||
|
const rewardRate = experienceMode === 'pvp-stadium-round-win-quarter-level'
|
||||||
|
? 0.25
|
||||||
|
: experienceMode === 'pvp-stadium-round-loss-tenth-level'
|
||||||
|
? 0.1
|
||||||
|
: 0.5
|
||||||
|
newExperience = Math.min(maxExperience, newExperience + Math.round(levelBand * rewardRate))
|
||||||
|
newLevel = database.prepare(`
|
||||||
|
SELECT MAX(level) AS level
|
||||||
|
FROM level_progression
|
||||||
|
WHERE experience_required <= ?
|
||||||
|
`).get(newExperience).level
|
||||||
} else {
|
} else {
|
||||||
const baseExperienceReward = Math.round(
|
const baseExperienceReward = Math.round(
|
||||||
dungeon.experienceReward * dungeon.experienceMultiplier * (encountersCleared / 3),
|
dungeon.experienceReward * dungeon.experienceMultiplier * (encountersCleared / 3),
|
||||||
@@ -2564,7 +2599,7 @@ function cleanupPvpMemory(now = Date.now()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validatePvpContentType(value) {
|
function validatePvpContentType(value) {
|
||||||
if (value !== 'dungeon' && value !== 'raid') {
|
if (value !== 'dungeon' && value !== 'raid' && value !== 'stadium') {
|
||||||
throw new Error('The PvP content type is invalid.')
|
throw new Error('The PvP content type is invalid.')
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
@@ -2736,7 +2771,7 @@ function requirePvpMatchForSession(session, matchId) {
|
|||||||
|
|
||||||
function updatePvpMatchState(session, matchId, payload) {
|
function updatePvpMatchState(session, matchId, payload) {
|
||||||
const { match, side } = requirePvpMatchForSession(session, matchId)
|
const { match, side } = requirePvpMatchForSession(session, matchId)
|
||||||
const status = ['playing', 'upgrade-choice', 'won', 'lost'].includes(payload.status)
|
const status = ['playing', 'upgrade-choice', 'shop', 'won', 'lost'].includes(payload.status)
|
||||||
? payload.status
|
? payload.status
|
||||||
: 'playing'
|
: 'playing'
|
||||||
const progress = {
|
const progress = {
|
||||||
@@ -2762,6 +2797,8 @@ function submitPvpUpgradeChoice(session, matchId, payload) {
|
|||||||
encounterIndex,
|
encounterIndex,
|
||||||
buffId: String(payload.buffId ?? ''),
|
buffId: String(payload.buffId ?? ''),
|
||||||
debuffId: String(payload.debuffId ?? ''),
|
debuffId: String(payload.debuffId ?? ''),
|
||||||
|
purchases: Array.isArray(payload.purchases) ? payload.purchases.map((purchase) => String(purchase)) : [],
|
||||||
|
shopReady: Boolean(payload.shopReady),
|
||||||
}
|
}
|
||||||
match.updatedAt = Date.now()
|
match.updatedAt = Date.now()
|
||||||
return pvpSnapshot(match)
|
return pvpSnapshot(match)
|
||||||
|
|||||||
+156
@@ -6399,6 +6399,149 @@ h2 {
|
|||||||
box-shadow: inset 0 0 0 2px #6e5727;
|
box-shadow: inset 0 0 0 2px #6e5727;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stadium-screen {
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-board {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
min-height: calc(100dvh - 72px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-header,
|
||||||
|
.stadium-pressure-panel {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 3px solid #0c0d11;
|
||||||
|
box-shadow: 4px 4px 0 #08090c;
|
||||||
|
display: flex;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: space-between;
|
||||||
|
outline: 2px solid var(--edge);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-header h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-header > strong {
|
||||||
|
color: var(--gold);
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-header p,
|
||||||
|
.stadium-header small,
|
||||||
|
.stadium-pressure-panel span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-pressure-panel strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-side {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-dialog {
|
||||||
|
max-width: 1180px !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
text-align: left !important;
|
||||||
|
width: min(1180px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-summary {
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: 240px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-tabs {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-tabs button,
|
||||||
|
.stadium-shop-grid button {
|
||||||
|
background: #252833;
|
||||||
|
border: 2px solid #0b0c0f;
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
outline: 2px solid #4d4c58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-tabs button {
|
||||||
|
font-size: 9px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-tabs button.active {
|
||||||
|
background: #303427;
|
||||||
|
outline-color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-layout h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-grid button {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 110px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-grid button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-grid strong {
|
||||||
|
color: #ffe8a5;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-grid small,
|
||||||
|
.stadium-shop-layout p {
|
||||||
|
color: #d3d9e6;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-opponent-progress.stadium {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.result-screen button,
|
.result-screen button,
|
||||||
.pause-screen button {
|
.pause-screen button {
|
||||||
background: var(--gold);
|
background: var(--gold);
|
||||||
@@ -6911,11 +7054,24 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pvp-board,
|
.pvp-board,
|
||||||
|
.stadium-board,
|
||||||
|
.stadium-shop-layout,
|
||||||
.pvp-choice-columns,
|
.pvp-choice-columns,
|
||||||
.pvp-choice-columns .upgrade-choice-grid {
|
.pvp-choice-columns .upgrade-choice-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stadium-header,
|
||||||
|
.stadium-pressure-panel,
|
||||||
|
.stadium-shop-summary {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stadium-shop-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.active-target-card,
|
.active-target-card,
|
||||||
.mana-wrap {
|
.mana-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+27
-4
@@ -5,6 +5,7 @@ import { AuthScreen } from './components/AuthScreen'
|
|||||||
import { CustomizeScreen } from './components/CustomizeScreen'
|
import { CustomizeScreen } from './components/CustomizeScreen'
|
||||||
import { EquipmentScreen } from './components/EquipmentScreen'
|
import { EquipmentScreen } from './components/EquipmentScreen'
|
||||||
import { PvPRoguelikeScreen } from './components/PvpRoguelikeScreen'
|
import { PvPRoguelikeScreen } from './components/PvpRoguelikeScreen'
|
||||||
|
import { PvpStadiumScreen } from './components/PvpStadiumScreen'
|
||||||
import { TalentScreen } from './components/TalentScreen'
|
import { TalentScreen } from './components/TalentScreen'
|
||||||
import { SettingsScreen } from './components/SettingsScreen'
|
import { SettingsScreen } from './components/SettingsScreen'
|
||||||
import {
|
import {
|
||||||
@@ -253,6 +254,19 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (screen === 'pvp') {
|
if (screen === 'pvp') {
|
||||||
|
if (pvpContentType === 'stadium') {
|
||||||
|
return (
|
||||||
|
<PvpStadiumScreen
|
||||||
|
gameMode={gameMode}
|
||||||
|
onExit={() => {
|
||||||
|
setRoguelikeVariant('pvp')
|
||||||
|
setScreen('roguelike')
|
||||||
|
}}
|
||||||
|
onProfileUpdated={setProfile}
|
||||||
|
profile={profile}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
const pvpPool = profile.dungeons
|
const pvpPool = profile.dungeons
|
||||||
.filter((candidate) => candidate.contentType === pvpContentType)
|
.filter((candidate) => candidate.contentType === pvpContentType)
|
||||||
.flatMap((candidate) => candidate.encounters)
|
.flatMap((candidate) => candidate.encounters)
|
||||||
@@ -547,6 +561,13 @@ function App() {
|
|||||||
>
|
>
|
||||||
Raid
|
Raid
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`text-button ${pvpContentType === 'stadium' ? 'active' : ''}`}
|
||||||
|
onClick={() => setPvpContentType('stadium')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Stadium
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-card pvp-queue-panel">
|
<div className="menu-card pvp-queue-panel">
|
||||||
@@ -554,9 +575,11 @@ function App() {
|
|||||||
<div>
|
<div>
|
||||||
<strong>{gameMode === 'offline' ? 'Offline CPU Match' : 'Queue Then CPU Fallback'}</strong>
|
<strong>{gameMode === 'offline' ? 'Offline CPU Match' : 'Queue Then CPU Fallback'}</strong>
|
||||||
<small>
|
<small>
|
||||||
{gameMode === 'offline'
|
{pvpContentType === 'stadium'
|
||||||
? 'Offline mode always places you against a random CPU 1-5.'
|
? 'Best-of-5 survival with dampening, equalized gear, and after-round buff buying.'
|
||||||
: 'Online mode searches briefly. If nobody is queued, a random CPU 1-5 takes the slot.'}
|
: gameMode === 'offline'
|
||||||
|
? 'Offline mode always places you against a random CPU 1-5.'
|
||||||
|
: 'Online mode searches briefly. If nobody is queued, a random CPU 1-5 takes the slot.'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -572,7 +595,7 @@ function App() {
|
|||||||
<div className="equipment-heading toggle-heading">
|
<div className="equipment-heading toggle-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">CPU Leaderboard</p>
|
<p className="eyebrow">CPU Leaderboard</p>
|
||||||
<h2>{pvpContentType === 'raid' ? 'Raid Clash' : 'Dungeon Clash'}</h2>
|
<h2>{pvpContentType === 'stadium' ? 'Stadium' : pvpContentType === 'raid' ? 'Raid Clash' : 'Dungeon Clash'}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="leaderboard-table">
|
<div className="leaderboard-table">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+36
-10
@@ -67,6 +67,14 @@ export type DualScreenCombatState = {
|
|||||||
paused: boolean
|
paused: boolean
|
||||||
targetGroup: 0 | 1 | 2
|
targetGroup: 0 | 1 | 2
|
||||||
speedMultiplier: 1 | 2
|
speedMultiplier: 1 | 2
|
||||||
|
stadium?: {
|
||||||
|
dampeningPercent: number
|
||||||
|
roundIndex: number
|
||||||
|
playerWins: number
|
||||||
|
opponentWins: number
|
||||||
|
survivalSeconds: number
|
||||||
|
opponentSurvivalSeconds: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DualScreenWorkshopState = {
|
export type DualScreenWorkshopState = {
|
||||||
@@ -138,6 +146,11 @@ function memberHotEffects(member: PartyMember) {
|
|||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDualTime(seconds: number) {
|
||||||
|
const total = Math.max(0, Math.floor(seconds))
|
||||||
|
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
export function DualScreenProvider({ children }: { children: ReactNode }) {
|
export function DualScreenProvider({ children }: { children: ReactNode }) {
|
||||||
const [enabled, setEnabledState] = useState(
|
const [enabled, setEnabledState] = useState(
|
||||||
() => localStorage.getItem(STORAGE_KEY) === 'true',
|
() => localStorage.getItem(STORAGE_KEY) === 'true',
|
||||||
@@ -446,21 +459,34 @@ export function DualScreenBottomDisplay() {
|
|||||||
{state.opponentParty && <small>{state.opponentClassName}</small>}
|
{state.opponentParty && <small>{state.opponentClassName}</small>}
|
||||||
</div>
|
</div>
|
||||||
<div className="dual-controls-progress">
|
<div className="dual-controls-progress">
|
||||||
<span>{state.opponentParty ? 'Live PvP' : `Encounter ${state.encounterIndex + 1}/${state.encounterCount}`}</span>
|
<span>{state.stadium ? `Round ${state.stadium.roundIndex} | ${state.stadium.playerWins}-${state.stadium.opponentWins}` : state.opponentParty ? 'Live PvP' : `Encounter ${state.encounterIndex + 1}/${state.encounterCount}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{state.opponentParty ? (
|
{state.opponentParty ? (
|
||||||
<>
|
<>
|
||||||
<section className="dual-opponent-progress">
|
{state.stadium ? (
|
||||||
<div>
|
<section className="dual-opponent-progress stadium">
|
||||||
<p className="eyebrow">Opponent Clear</p>
|
<div>
|
||||||
<strong>{Math.max(0, Math.floor(state.opponentEnemyHealth ?? 0))} / {state.encounterMaxHealth}</strong>
|
<p className="eyebrow">Dampening</p>
|
||||||
</div>
|
<strong>{state.stadium.dampeningPercent}%</strong>
|
||||||
<div className="bar enemy-health boss-bar">
|
</div>
|
||||||
<span style={{ width: `${Math.max(0, ((state.opponentEnemyHealth ?? 0) / state.encounterMaxHealth) * 100)}%` }} />
|
<div>
|
||||||
</div>
|
<p className="eyebrow">Survival</p>
|
||||||
</section>
|
<strong>{formatDualTime(state.stadium.opponentSurvivalSeconds)}</strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section className="dual-opponent-progress">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Opponent Clear</p>
|
||||||
|
<strong>{Math.max(0, Math.floor(state.opponentEnemyHealth ?? 0))} / {state.encounterMaxHealth}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="bar enemy-health boss-bar">
|
||||||
|
<span style={{ width: `${Math.max(0, ((state.opponentEnemyHealth ?? 0) / state.encounterMaxHealth) * 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className={`dual-opponent-party-grid ${state.opponentParty.length > 6 ? 'raid' : ''}`}>
|
<section className={`dual-opponent-party-grid ${state.opponentParty.length > 6 ? 'raid' : ''}`}>
|
||||||
{state.opponentParty.map((member) => (
|
{state.opponentParty.map((member) => (
|
||||||
|
|||||||
+29
-2
@@ -37,7 +37,7 @@ export interface GameRepository {
|
|||||||
durationSeconds: number,
|
durationSeconds: number,
|
||||||
options?: {
|
options?: {
|
||||||
bossesCleared?: number
|
bossesCleared?: number
|
||||||
experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level'
|
experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level' | 'pvp-stadium-round-win-quarter-level' | 'pvp-stadium-round-loss-tenth-level' | 'pvp-stadium-match-half-level'
|
||||||
fightsCleared?: number
|
fightsCleared?: number
|
||||||
lootSourceEncounterId?: number
|
lootSourceEncounterId?: number
|
||||||
roguelikeStage?: number
|
roguelikeStage?: number
|
||||||
@@ -529,6 +529,27 @@ function scaledPvpFightExperience(
|
|||||||
return { experience, level }
|
return { experience, level }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scaledCurrentLevelExperience(
|
||||||
|
startingExperience: number,
|
||||||
|
startingLevel: number,
|
||||||
|
maxLevel: number,
|
||||||
|
rate: number,
|
||||||
|
) {
|
||||||
|
let experience = startingExperience
|
||||||
|
let level = startingLevel
|
||||||
|
const maxExperience = experienceForLevel(maxLevel)
|
||||||
|
const currentLevelFloor = experienceForLevel(level)
|
||||||
|
const nextLevelExperience = level >= maxLevel
|
||||||
|
? maxExperience
|
||||||
|
: experienceForLevel(level + 1)
|
||||||
|
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
|
||||||
|
experience = Math.min(maxExperience, experience + Math.round(levelBand * rate))
|
||||||
|
while (level < maxLevel && experienceForLevel(level + 1) <= experience) {
|
||||||
|
level += 1
|
||||||
|
}
|
||||||
|
return { experience, level }
|
||||||
|
}
|
||||||
|
|
||||||
function talentEffectCapacity(level: number) {
|
function talentEffectCapacity(level: number) {
|
||||||
return Math.min(4, Math.max(0, Math.floor(level / 5)))
|
return Math.min(4, Math.max(0, Math.floor(level / 5)))
|
||||||
}
|
}
|
||||||
@@ -1176,7 +1197,13 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
|||||||
profile.maxLevel,
|
profile.maxLevel,
|
||||||
highestOtherClassLevel(save),
|
highestOtherClassLevel(save),
|
||||||
)
|
)
|
||||||
: null
|
: options?.experienceMode === 'pvp-stadium-round-win-quarter-level'
|
||||||
|
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.25)
|
||||||
|
: options?.experienceMode === 'pvp-stadium-round-loss-tenth-level'
|
||||||
|
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.1)
|
||||||
|
: options?.experienceMode === 'pvp-stadium-match-half-level'
|
||||||
|
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.5)
|
||||||
|
: null
|
||||||
const baseRoguelikeReward = Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3))
|
const baseRoguelikeReward = Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3))
|
||||||
const newExperience = scaledReward
|
const newExperience = scaledReward
|
||||||
? scaledReward.experience
|
? scaledReward.experience
|
||||||
|
|||||||
+1
-1
@@ -349,7 +349,7 @@ export async function completeRoguelike(
|
|||||||
durationSeconds: number,
|
durationSeconds: number,
|
||||||
options?: {
|
options?: {
|
||||||
bossesCleared?: number
|
bossesCleared?: number
|
||||||
experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level'
|
experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level' | 'pvp-stadium-round-win-quarter-level' | 'pvp-stadium-round-loss-tenth-level' | 'pvp-stadium-match-half-level'
|
||||||
fightsCleared?: number
|
fightsCleared?: number
|
||||||
lootSourceEncounterId?: number
|
lootSourceEncounterId?: number
|
||||||
roguelikeStage?: number
|
roguelikeStage?: number
|
||||||
|
|||||||
+6
-3
@@ -1,8 +1,9 @@
|
|||||||
import { requestGameApiJson } from './gameRepository'
|
import { requestGameApiJson } from './gameRepository'
|
||||||
|
|
||||||
export type PvpContentType = 'dungeon' | 'raid'
|
export type PvpContentType = 'dungeon' | 'raid' | 'stadium'
|
||||||
export type CpuDifficulty = 1 | 2 | 3 | 4 | 5
|
export type CpuDifficulty = 1 | 2 | 3 | 4 | 5
|
||||||
export type PvpMatchSide = 'a' | 'b'
|
export type PvpMatchSide = 'a' | 'b'
|
||||||
|
export type PvpMatchStatus = 'playing' | 'upgrade-choice' | 'shop' | 'won' | 'lost'
|
||||||
|
|
||||||
export type PvpPlayerInfo = {
|
export type PvpPlayerInfo = {
|
||||||
side: PvpMatchSide
|
side: PvpMatchSide
|
||||||
@@ -16,6 +17,8 @@ export type PvpUpgradeChoicePayload = {
|
|||||||
encounterIndex: number
|
encounterIndex: number
|
||||||
buffId: string
|
buffId: string
|
||||||
debuffId: string
|
debuffId: string
|
||||||
|
purchases?: string[]
|
||||||
|
shopReady?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PvpMatchSnapshot<TSideState = unknown> = {
|
export type PvpMatchSnapshot<TSideState = unknown> = {
|
||||||
@@ -25,7 +28,7 @@ export type PvpMatchSnapshot<TSideState = unknown> = {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
players: Record<PvpMatchSide, PvpPlayerInfo>
|
players: Record<PvpMatchSide, PvpPlayerInfo>
|
||||||
states: Partial<Record<PvpMatchSide, TSideState>>
|
states: Partial<Record<PvpMatchSide, TSideState>>
|
||||||
statuses: Partial<Record<PvpMatchSide, 'playing' | 'upgrade-choice' | 'won' | 'lost'>>
|
statuses: Partial<Record<PvpMatchSide, PvpMatchStatus>>
|
||||||
progress: Partial<Record<PvpMatchSide, {
|
progress: Partial<Record<PvpMatchSide, {
|
||||||
stage: number
|
stage: number
|
||||||
encounterIndex: number
|
encounterIndex: number
|
||||||
@@ -143,7 +146,7 @@ export function publishPvpMatchState<TSideState>(
|
|||||||
matchId: string,
|
matchId: string,
|
||||||
payload: {
|
payload: {
|
||||||
state: TSideState
|
state: TSideState
|
||||||
status: 'playing' | 'upgrade-choice' | 'won' | 'lost'
|
status: PvpMatchStatus
|
||||||
stage: number
|
stage: number
|
||||||
encounterIndex: number
|
encounterIndex: number
|
||||||
encountersCleared: number
|
encountersCleared: number
|
||||||
|
|||||||
Reference in New Issue
Block a user