Android build v1.1.9
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { CharacterProfile, DungeonEncounter } from '../profile'
|
||||
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
|
||||
|
||||
type HunterProfileScreenProps = {
|
||||
profile: CharacterProfile
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
type BossEntry = {
|
||||
encounter: DungeonEncounter
|
||||
dungeonId: number
|
||||
dungeonName: string
|
||||
contentType: 'dungeon' | 'raid'
|
||||
}
|
||||
|
||||
type CollectionItem = {
|
||||
key: string
|
||||
glyph: string
|
||||
name: string
|
||||
chance: string
|
||||
quantity: number
|
||||
rarity: 'stat' | 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
|
||||
source: string
|
||||
}
|
||||
|
||||
function bossInitials(name: string) {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase() ?? '')
|
||||
.join('')
|
||||
}
|
||||
|
||||
function dropChanceLabel(chance: number) {
|
||||
if (chance <= 0) return 'Unavailable'
|
||||
const denominator = Math.round(1 / chance)
|
||||
if (denominator >= 10) return `1 in ${denominator}`
|
||||
return `${Math.round(chance * 100)}%`
|
||||
}
|
||||
|
||||
function purplePetKey(encounterId: number) {
|
||||
return `purple:${encounterId}`
|
||||
}
|
||||
|
||||
export function HunterProfileScreen({ profile, onBack }: HunterProfileScreenProps) {
|
||||
const [activeTab, setActiveTab] = useState<'stats' | 'collection'>('stats')
|
||||
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||
const bosses = useMemo<BossEntry[]>(() => profile.dungeons.flatMap((dungeon) =>
|
||||
dungeon.encounters
|
||||
.filter((encounter) => encounter.isBoss)
|
||||
.map((encounter) => ({
|
||||
encounter,
|
||||
dungeonId: dungeon.id,
|
||||
dungeonName: dungeon.name,
|
||||
contentType: dungeon.contentType,
|
||||
})),
|
||||
), [profile.dungeons])
|
||||
const [selectedBossId, setSelectedBossId] = useState(() => bosses[0]?.encounter.id ?? 0)
|
||||
const [focusedItemKey, setFocusedItemKey] = useState<string | null>(null)
|
||||
const selectedBoss = bosses.find((boss) => boss.encounter.id === selectedBossId) ?? bosses[0]
|
||||
const bossKills = profile.hunterStats?.bossKills ?? {}
|
||||
const bossPets = profile.hunterStats?.bossPets ?? {}
|
||||
const inventoryQuantities = useMemo(
|
||||
() => new Map(profile.inventory.map((item) => [item.id, item.quantity])),
|
||||
[profile.inventory],
|
||||
)
|
||||
const totalBossKills = bosses.reduce((total, boss) => total + (bossKills[String(boss.encounter.id)] ?? 0), 0)
|
||||
const mostKilledBoss = bosses.reduce<BossEntry | null>((best, boss) => {
|
||||
if (!best) return boss
|
||||
return (bossKills[String(boss.encounter.id)] ?? 0) > (bossKills[String(best.encounter.id)] ?? 0)
|
||||
? boss
|
||||
: best
|
||||
}, null)
|
||||
const matchesPlayed = profile.hunterStats?.pvpMatchesPlayed ?? 0
|
||||
const matchesWon = profile.hunterStats?.pvpMatchesWon ?? 0
|
||||
const winRate = matchesPlayed > 0 ? Math.round((matchesWon / matchesPlayed) * 100) : 0
|
||||
const collectionItems = useMemo<CollectionItem[]>(() => {
|
||||
if (!selectedBoss) return []
|
||||
return [
|
||||
{
|
||||
key: `kills:${selectedBoss.encounter.id}`,
|
||||
glyph: 'K',
|
||||
name: 'Boss Kills',
|
||||
chance: 'Defeats',
|
||||
quantity: bossKills[String(selectedBoss.encounter.id)] ?? 0,
|
||||
rarity: 'stat',
|
||||
source: selectedBoss.encounter.enemyName,
|
||||
},
|
||||
...selectedBoss.encounter.lootTables.map((drop) => ({
|
||||
key: `drop:${drop.difficultyId}:${drop.id}`,
|
||||
glyph: drop.glyph,
|
||||
name: drop.name,
|
||||
chance: dropChanceLabel(drop.dropChance),
|
||||
quantity: inventoryQuantities.get(drop.id) ?? 0,
|
||||
rarity: drop.rarity,
|
||||
source: selectedBoss.encounter.enemyName,
|
||||
})),
|
||||
{
|
||||
key: `pet:${selectedBoss.encounter.id}`,
|
||||
glyph: '*',
|
||||
name: `${selectedBoss.encounter.enemyName} Pet`,
|
||||
chance: '1 in 500',
|
||||
quantity: bossPets[String(selectedBoss.encounter.id)] ?? 0,
|
||||
rarity: 'legendary',
|
||||
source: selectedBoss.encounter.enemyName,
|
||||
},
|
||||
{
|
||||
key: `purple-pet:${selectedBoss.encounter.id}`,
|
||||
glyph: 'P',
|
||||
name: `Purple ${selectedBoss.encounter.enemyName} Pet`,
|
||||
chance: '1 in 500',
|
||||
quantity: bossPets[purplePetKey(selectedBoss.encounter.id)] ?? 0,
|
||||
rarity: 'epic',
|
||||
source: `${selectedBoss.encounter.enemyName} Roguelike`,
|
||||
},
|
||||
]
|
||||
}, [bossKills, bossPets, inventoryQuantities, selectedBoss])
|
||||
const focusedItem = collectionItems.find((item) => item.key === focusedItemKey) ?? collectionItems[0] ?? null
|
||||
const itemDetailState = useMemo<DualScreenWorkshopState | null>(() => {
|
||||
if (activeTab !== 'collection' || !selectedBoss || !focusedItem) return null
|
||||
return {
|
||||
mode: 'collection',
|
||||
title: focusedItem.name,
|
||||
subtitle: selectedBoss.encounter.enemyName,
|
||||
summary: `Owned x${focusedItem.quantity}`,
|
||||
items: [
|
||||
{
|
||||
glyph: focusedItem.glyph,
|
||||
title: 'Drop Rate',
|
||||
meta: focusedItem.chance,
|
||||
detail: focusedItem.source,
|
||||
status: focusedItem.quantity > 0 ? 'Collected' : 'Missing',
|
||||
},
|
||||
{
|
||||
title: 'Boss Kills',
|
||||
meta: `${bossKills[String(selectedBoss.encounter.id)] ?? 0}`,
|
||||
detail: selectedBoss.dungeonName,
|
||||
},
|
||||
],
|
||||
}
|
||||
}, [activeTab, bossKills, focusedItem, selectedBoss])
|
||||
useDualScreenWorkshopPublisher(itemDetailState, dualScreenEnabled)
|
||||
|
||||
return (
|
||||
<section className="content-screen hunter-profile-screen">
|
||||
<div className="screen-heading hunter-profile-heading">
|
||||
<div className="equipment-tabs hunter-profile-tabs" role="tablist" aria-label="Hunter profile tabs">
|
||||
<button
|
||||
className={`equipment-tab ${activeTab === 'stats' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('stats')}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'stats'}
|
||||
>
|
||||
Stats
|
||||
</button>
|
||||
<button
|
||||
className={`equipment-tab ${activeTab === 'collection' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('collection')}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'collection'}
|
||||
>
|
||||
Collection Log
|
||||
</button>
|
||||
</div>
|
||||
<button className="back-button" onClick={onBack} type="button">Back</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'stats' && (
|
||||
<div className="hunter-stat-grid">
|
||||
<article className="hunter-stat-tile">
|
||||
<span>Total Boss Kills</span>
|
||||
<strong>{totalBossKills}</strong>
|
||||
</article>
|
||||
<article className="hunter-stat-tile">
|
||||
<span>Most Killed Boss</span>
|
||||
<strong>{totalBossKills > 0 && mostKilledBoss ? mostKilledBoss.encounter.enemyName : 'None'}</strong>
|
||||
<small>{totalBossKills > 0 && mostKilledBoss ? `${bossKills[String(mostKilledBoss.encounter.id)] ?? 0} kills` : '0 kills'}</small>
|
||||
</article>
|
||||
<article className="hunter-stat-tile">
|
||||
<span>PvP Matches</span>
|
||||
<strong>{matchesPlayed}</strong>
|
||||
</article>
|
||||
<article className="hunter-stat-tile">
|
||||
<span>PvP Wins</span>
|
||||
<strong>{matchesWon}</strong>
|
||||
</article>
|
||||
<article className="hunter-stat-tile">
|
||||
<span>PvP Win Rate</span>
|
||||
<strong>{winRate}%</strong>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'collection' && (
|
||||
<div className="collection-log-layout">
|
||||
<div className="collection-boss-list" aria-label="Bosses">
|
||||
{bosses.map((boss) => {
|
||||
return (
|
||||
<button
|
||||
className={`collection-boss-button ${selectedBoss?.encounter.id === boss.encounter.id ? 'selected' : ''}`}
|
||||
key={boss.encounter.id}
|
||||
onClick={() => setSelectedBossId(boss.encounter.id)}
|
||||
type="button"
|
||||
>
|
||||
<span>{bossInitials(boss.encounter.enemyName)}</span>
|
||||
<strong>{boss.encounter.enemyName}</strong>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{selectedBoss && (
|
||||
<article className="collection-boss-detail">
|
||||
<div className="collection-drop-list">
|
||||
{collectionItems.map((item) => (
|
||||
<button
|
||||
className={`collection-drop-row collection-rarity-${item.rarity} ${item.quantity <= 0 ? 'missing' : 'owned'}`}
|
||||
key={item.key}
|
||||
onClick={() => setFocusedItemKey(item.key)}
|
||||
onFocus={() => setFocusedItemKey(item.key)}
|
||||
type="button"
|
||||
>
|
||||
<span>{item.glyph}</span>
|
||||
<strong>{item.name}</strong>
|
||||
<small>{item.chance}</small>
|
||||
<b>x{item.quantity}</b>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user