1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 15:45:14 +08:00
parent 8a7bd90458
commit 1c72066bab
73 changed files with 7814 additions and 1018 deletions

View File

@@ -44,6 +44,7 @@ import {
createNpcBattleMonster,
normalizeNpcPersistentState,
} from '../data/npcInteractions';
import { normalizePlayerProgressionState } from '../data/playerProgression';
import { getSceneHostileNpcPresetIds } from '../data/scenePresets';
import type { CharacterChatTarget } from '../hooks/useStoryGeneration';
import { getResourceLabelsForWorld } from '../services/customWorldPresentation';
@@ -64,6 +65,7 @@ import {
} from './BackstoryArchive';
import { CharacterAnimator } from './CharacterAnimator';
import {
buildCharacterSkillRenderId,
getCharacterDetailSpriteStyle,
getContributionVisualStyle,
getSkillDeliveryLabel,
@@ -71,8 +73,10 @@ import {
} from './CharacterInfoHelpers';
import {
CharacterAttributeGrid,
CharacterIdentityBadges,
CharacterSkillsList,
MultiplierContributionList,
PlayerLevelProgress,
StatusRow,
} from './CharacterInfoShared';
import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared';
@@ -137,7 +141,8 @@ function resolveSkillPreviewMonsterId(gameState: GameState) {
return null;
}
const sceneMonsterId = getSceneHostileNpcPresetIds(gameState.currentScenePreset)[0] ?? null;
const sceneMonsterId =
getSceneHostileNpcPresetIds(gameState.currentScenePreset)[0] ?? null;
if (sceneMonsterId) {
return sceneMonsterId;
}
@@ -469,6 +474,45 @@ export function AdventureEntityModal({
privateChatUnlockAffinity != null &&
companionNpcState.affinity >= privateChatUnlockAffinity,
);
const normalizedPlayerProgression = normalizePlayerProgressionState(
gameState.playerProgression ?? null,
);
const selectedNpcLevel =
npcEncounter?.levelProfile?.level ??
npcBattleState?.levelProfile?.level ??
null;
const selectionRoleLabel =
selection?.kind === 'player'
? '队长'
: selection?.kind === 'companion'
? '同行'
: selection?.kind === 'npc' && npcEncounter && npcState
? getNpcBadge(
npcEncounter,
npcState.affinity,
Boolean(npcBattleState),
)
: null;
const selectionLevelText =
selection?.kind === 'player'
? `Lv.${normalizedPlayerProgression.level}`
: selection?.kind === 'companion'
? `参考 Lv.${normalizedPlayerProgression.level}`
: typeof selectedNpcLevel === 'number'
? `Lv.${selectedNpcLevel}`
: null;
const selectionRoleTone: 'amber' | 'sky' | 'rose' | 'emerald' | 'zinc' =
selection?.kind === 'player'
? 'amber'
: selection?.kind === 'companion'
? 'sky'
: selection?.kind === 'npc' && npcEncounter && npcState
? npcEncounter.hostile ||
Boolean(npcBattleState) ||
npcState.affinity < 0
? 'rose'
: 'emerald'
: 'zinc';
const title =
selection?.kind === 'player'
@@ -673,7 +717,10 @@ export function AdventureEntityModal({
)
: [];
const selectedSkill =
displayedSkills.find((skill) => skill.id === selectedSkillId) ?? null;
displayedSkills.find(
(skill, index) =>
buildCharacterSkillRenderId(skill, index) === selectedSkillId,
) ?? null;
const selectedSkillPreviewWorldType = gameState.worldType ?? null;
const selectedSkillPreviewMonsterId = selectedSkillPreviewWorldType
? resolveSkillPreviewMonsterId(gameState)
@@ -686,23 +733,30 @@ export function AdventureEntityModal({
inventory.find((item) => item.id === selectedItemId) ?? null;
const selectedSkillOwnerName =
detailCharacter?.name ?? npcEncounter?.npcName ?? title;
const recentChronicleEntries = gameState.storyEngineMemory?.chronicle?.slice(-3) ?? [];
const recentCarrierEchoes = (gameState.storyEngineMemory?.recentCarrierIds ?? [])
.map((carrierId) =>
gameState.playerInventory.find((item) => item.id === carrierId)?.runtimeMetadata?.storyFingerprint?.visibleClue
?? gameState.playerInventory.find((item) => item.id === carrierId)?.name
?? '',
const recentChronicleEntries =
gameState.storyEngineMemory?.chronicle?.slice(-3) ?? [];
const recentCarrierEchoes = (
gameState.storyEngineMemory?.recentCarrierIds ?? []
)
.map(
(carrierId) =>
gameState.playerInventory.find((item) => item.id === carrierId)
?.runtimeMetadata?.storyFingerprint?.visibleClue ??
gameState.playerInventory.find((item) => item.id === carrierId)?.name ??
'',
)
.filter(Boolean)
.slice(0, 3);
const sceneResidues = gameState.currentScenePreset?.narrativeResidues?.slice(0, 3) ?? [];
const selectedCompanionResolution =
detailCharacter
? gameState.storyEngineMemory?.companionResolutions?.find(
(resolution) => resolution.characterId === detailCharacter.id,
) ?? null
: null;
const relatedConsequences = (gameState.storyEngineMemory?.consequenceLedger ?? [])
const sceneResidues =
gameState.currentScenePreset?.narrativeResidues?.slice(0, 3) ?? [];
const selectedCompanionResolution = detailCharacter
? (gameState.storyEngineMemory?.companionResolutions?.find(
(resolution) => resolution.characterId === detailCharacter.id,
) ?? null)
: null;
const relatedConsequences = (
gameState.storyEngineMemory?.consequenceLedger ?? []
)
.filter((record) =>
detailCharacter
? record.relatedIds.includes(detailCharacter.id)
@@ -761,6 +815,14 @@ export function AdventureEntityModal({
{title}
</div>
<div className="mt-1 text-sm text-zinc-400">{subtitle}</div>
{selectionRoleLabel ? (
<CharacterIdentityBadges
roleLabel={selectionRoleLabel}
levelText={selectionLevelText}
roleTone={selectionRoleTone}
className="mt-2"
/>
) : null}
</div>
<button
type="button"
@@ -780,7 +842,9 @@ export function AdventureEntityModal({
{selection.kind === 'player' && playerCharacter ? (
playerCharacter.visual ? (
<MedievalNpcAnimator
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(playerCharacter.visual)}
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
playerCharacter.visual,
)}
scale={2.08}
/>
) : (
@@ -798,7 +862,9 @@ export function AdventureEntityModal({
companionCharacter ? (
companionCharacter.visual ? (
<MedievalNpcAnimator
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(companionCharacter.visual)}
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
companionCharacter.visual,
)}
scale={2.08}
/>
) : (
@@ -815,7 +881,9 @@ export function AdventureEntityModal({
) : npcCharacter ? (
npcCharacter.visual ? (
<MedievalNpcAnimator
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(npcCharacter.visual)}
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
npcCharacter.visual,
)}
scale={2.08}
/>
) : (
@@ -824,7 +892,9 @@ export function AdventureEntityModal({
character={npcCharacter}
className="h-full w-full"
imageClassName="object-bottom"
style={getCharacterDetailSpriteStyle(npcCharacter)}
style={getCharacterDetailSpriteStyle(
npcCharacter,
)}
/>
)
) : hostileNpcPreset ? (
@@ -842,15 +912,6 @@ export function AdventureEntityModal({
/>
) : null}
</div>
{selection.kind === 'npc' && npcEncounter && npcState && (
<div className="mt-3 rounded-full border border-rose-400/25 bg-rose-500/10 px-3 py-1 text-[10px] tracking-[0.18em] text-rose-100">
{getNpcBadge(
npcEncounter,
npcState.affinity,
Boolean(npcBattleState),
)}
</div>
)}
<p className="mt-3 text-sm leading-relaxed text-zinc-300">
{description}
</p>
@@ -942,17 +1003,24 @@ export function AdventureEntityModal({
<div className="space-y-3">
{selectedCompanionResolution && (
<div className="rounded-xl border border-emerald-400/18 bg-emerald-500/8 px-3 py-2 text-xs text-emerald-100/85">
{selectedCompanionResolution.resolutionType} · {selectedCompanionResolution.summary}
{selectedCompanionResolution.resolutionType} ·{' '}
{selectedCompanionResolution.summary}
</div>
)}
{relatedConsequences.length > 0 && (
<div className="space-y-1">
{relatedConsequences.map((record, index) => (
<div
key={record.id || `consequence-${record.title}-${index}`}
key={
record.id ||
`consequence-${record.title}-${index}`
}
className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs text-zinc-400"
>
<span className="text-white">{record.title}</span>
<span className="text-white">
{record.title}
</span>
{''}
{record.summary}
</div>
@@ -963,7 +1031,10 @@ export function AdventureEntityModal({
<div className="space-y-1">
{recentChronicleEntries.map((entry, index) => (
<div
key={entry.id || `chronicle-${entry.title}-${index}`}
key={
entry.id ||
`chronicle-${entry.title}-${index}`
}
className="rounded-xl border border-white/8 bg-black/20 px-3 py-2"
>
<div className="text-sm font-medium text-white">
@@ -985,10 +1056,15 @@ export function AdventureEntityModal({
<div className="space-y-1">
{sceneResidues.map((residue, index) => (
<div
key={residue.id || `residue-${residue.title}-${index}`}
key={
residue.id ||
`residue-${residue.title}-${index}`
}
className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs text-zinc-400"
>
<span className="text-white">{residue.title}</span>
<span className="text-white">
{residue.title}
</span>
{''}
{residue.visibleClue}
</div>
@@ -1001,6 +1077,22 @@ export function AdventureEntityModal({
<Section title="属性">
<div className="space-y-4">
{selection.kind === 'player' ? (
<div className="rounded-xl border border-amber-300/18 bg-amber-500/8 px-3 py-3">
<div className="mb-2 text-[10px] uppercase tracking-[0.18em] text-amber-100/75">
</div>
<PlayerLevelProgress
level={normalizedPlayerProgression.level}
currentLevelXp={
normalizedPlayerProgression.currentLevelXp
}
xpToNextLevel={
normalizedPlayerProgression.xpToNextLevel
}
/>
</div>
) : null}
<div className="space-y-3">
<StatusRow
label={resourceLabels.hp}