import { resolveRoleCombatStats } from '../data/attributeCombat'; import { getAttributeSlotValue } from '../data/attributeResolver'; import { type BuildContributionAttributeRow, type BuildDamageBreakdown, formatBuildContributionPercent, getBuildContributionQualityLabel, } from '../data/buildDamage'; import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; import type { Character, RoleAttributeProfile, WorldAttributeSchema, } from '../types'; import { buildCharacterSkillRenderId, type ContributionRow, formatAttributeMetricValue, getAttributeBonusPillClassName, getAttributeEffectText, getContributionVisualStyle, getSkillDeliveryLabel, getSkillStyleLabel, } from './CharacterInfoHelpers'; import { PlatformEmptyState } from './common/PlatformEmptyState'; import { PlatformPillBadge } from './common/PlatformPillBadge'; import type { PlatformPillBadgeTone } from './common/platformPillBadgeModel'; import { PlatformSubpanel } from './common/PlatformSubpanel'; export function StatusRow({ label, current, max, tone, }: { label: string; current: number; max: number; tone: 'hp' | 'mp'; }) { const ratio = Math.max(0, Math.min(1, max > 0 ? current / max : 0)); const fillClass = tone === 'hp' ? 'from-emerald-400 via-lime-300 to-emerald-200' : 'from-sky-500 via-cyan-300 to-sky-100'; return (
{label} {current} / {max}
); } export function CharacterIdentityBadges({ roleLabel, levelText = null, roleTone = 'sky', className = '', }: { roleLabel: string; levelText?: string | null; roleTone?: 'amber' | 'sky' | 'rose' | 'emerald' | 'zinc'; className?: string; }) { const roleBadgeTone: PlatformPillBadgeTone = roleTone === 'amber' ? 'darkAmber' : roleTone === 'rose' ? 'darkRose' : roleTone === 'emerald' ? 'darkEmerald' : roleTone === 'zinc' ? 'darkNeutral' : 'darkSky'; return (
{roleLabel} {levelText ? ( {levelText} ) : null}
); } export function PlayerLevelProgress({ level, currentLevelXp, xpToNextLevel, className = '', }: { level: number; currentLevelXp: number; xpToNextLevel: number; className?: string; }) { const safeLevel = Math.max(1, Math.round(level)); const safeCurrentLevelXp = Math.max(0, Math.round(currentLevelXp)); const safeXpToNextLevel = Math.max(0, Math.round(xpToNextLevel)); const ratio = safeXpToNextLevel <= 0 ? 1 : Math.max(0, Math.min(1, safeCurrentLevelXp / safeXpToNextLevel)); return (
Lv.{safeLevel}
{safeXpToNextLevel > 0 ? `${safeCurrentLevelXp}/${safeXpToNextLevel}` : 'MAX'}
); } export function CharacterSkillsList({ skills, onSelectSkill, emptyText = '暂无技能信息', }: { skills: Character['skills']; onSelectSkill?: ((skillId: string) => void) | null; emptyText?: string; }) { if (skills.length === 0) { return ( {emptyText} ); } return (
{skills.map((skill, index) => { const skillRenderId = buildCharacterSkillRenderId(skill, index); const content = ( <>
{skill.name}
{getSkillDeliveryLabel(skill)}
伤害:{skill.damage}
法力:{skill.manaCost}
冷却:{skill.cooldownTurns}
距离:{skill.range}
{getSkillStyleLabel(skill)}
); if (onSelectSkill) { return ( onSelectSkill(skillRenderId)} surface="dark" radius="xs" padding="sm" className="text-left text-sm text-zinc-300 transition-colors hover:border-sky-300/25 hover:bg-sky-500/8" > {content} ); } return ( {content} ); })}
); } export function MultiplierContributionList({ breakdown, onSelectContribution, }: { breakdown: BuildDamageBreakdown; onSelectContribution: (row: ContributionRow) => void; }) { const sortedRows = [...breakdown.rows].sort( (left, right) => right.bonusDelta - left.bonusDelta || left.label.localeCompare(right.label, 'zh-CN'), ); return (
状态标签 点击标签查看具体属性加成
{sortedRows.length > 0 ? (
{sortedRows.map((row) => ( ))}
) : ( 当前还没有形成有效标签 )}
); } /** * 角色构筑标签详情面板。 * 统一承接队伍面板和实体详情弹窗里的标签概览、属性加成与空明细外壳。 */ export function BuildContributionDetailPanel({ row, attributes, emptyText = '当前标签还没有可展示的属性适配明细。', }: { row: ContributionRow; attributes: BuildContributionAttributeRow[]; emptyText?: string; }) { return (
标签概览
{row.label}
{getBuildContributionQualityLabel(row.bonusDelta)}
总加成 {formatBuildContributionPercent(row.bonusDelta)}
属性加成
{attributes.length > 0 ? (
{attributes.map((attribute) => (
{attribute.label} {formatBuildContributionPercent(attribute.modifierDelta)}
))}
) : ( {emptyText} )}
); } export function CharacterAttributeGrid({ attributeProfile, attributeSchema, buildBreakdown = null, resourceLabels, emptyText = '暂无属性信息', gridClassName = 'grid grid-cols-1 gap-2 text-sm text-zinc-300 sm:grid-cols-2', cardClassName = 'rounded-xl border border-white/8 bg-black/25 px-3 py-2', }: { attributeProfile: RoleAttributeProfile | null | undefined; attributeSchema: WorldAttributeSchema; buildBreakdown?: BuildDamageBreakdown | null; resourceLabels: ReturnType; emptyText?: string; gridClassName?: string; cardClassName?: string; }) { const attributeRows = attributeSchema.slots.map((slot) => ({ slot, value: getAttributeSlotValue(attributeProfile, slot.slotId), })); const attributeBonusBySlot = Object.fromEntries( attributeSchema.slots.map((slot) => [ slot.slotId, Number( ( buildBreakdown?.rows.reduce( (sum, row) => sum + (row.attributeModifierDeltas?.[slot.slotId] ?? 0), 0, ) ?? 0 ).toFixed(4), ), ]), ) as Record; const boostedAttributeProfile = attributeProfile ? { ...attributeProfile, values: { ...(attributeProfile.values ?? {}), ...Object.fromEntries( attributeSchema.slots.map((slot) => { const baseValue = attributeProfile.values?.[slot.slotId] ?? 0; const totalBonus = attributeBonusBySlot[slot.slotId] ?? 0; return [ slot.slotId, Number((baseValue * (1 + totalBonus)).toFixed(4)), ]; }), ), }, } : null; const boostedCombatStats = boostedAttributeProfile ? resolveRoleCombatStats(boostedAttributeProfile) : null; const displayRows = attributeRows.map(({ slot, value }) => { const totalBonus = attributeBonusBySlot[slot.slotId] ?? 0; const boostedValue = Number((value * (1 + totalBonus)).toFixed(4)); return { slot, baseValue: value, boostedValue, totalBonus, effectText: boostedCombatStats ? getAttributeEffectText( slot.slotId, boostedCombatStats, resourceLabels, ) : '', }; }); if (displayRows.length === 0) { return
{emptyText}
; } return (
{displayRows.map( ({ slot, baseValue, boostedValue, totalBonus, effectText }) => (
{slot.name}
{formatAttributeMetricValue(boostedValue)}
标签加成 {formatBuildContributionPercent(totalBonus)}
原始 {formatAttributeMetricValue(baseValue)}
{effectText ? (
{effectText}
) : null}
), )}
); }