fix: preserve rpg custom world detail profiles

This commit is contained in:
kdletters
2026-05-22 03:14:11 +08:00
parent a9d23a8a44
commit d74457faa2
19 changed files with 2726 additions and 109 deletions

View File

@@ -7,7 +7,10 @@ import {
type Encounter,
type SceneHostileNpc,
} from '../../types';
import { GameCanvasEntityLayer } from './GameCanvasEntityLayer';
import {
GameCanvasEntityLayer,
getCombatFloatingNumberPresentation,
} from './GameCanvasEntityLayer';
import {
CHARACTER_COMBAT_HP_TOP_PX,
ENTITY_CONTAINER_REM,
@@ -125,6 +128,21 @@ function renderEntityLayer(effectNpcId: string | null) {
}
describe('GameCanvasEntityLayer', () => {
it('keeps combat floating numbers readable on dark noisy battle backgrounds', () => {
const damage = getCombatFloatingNumberPresentation(false);
const healing = getCombatFloatingNumberPresentation(true);
expect(damage.toneClass).toContain('bg-rose-950/72');
expect(damage.toneClass).toContain('text-rose-50');
expect(damage.textStyle.WebkitTextStroke).toContain('rgba(127, 29, 29');
expect(damage.textStyle.textShadow).toContain('rgba(0, 0, 0');
expect(healing.toneClass).toContain('bg-emerald-950/70');
expect(healing.toneClass).toContain('text-emerald-50');
expect(healing.textStyle.WebkitTextStroke).toContain('rgba(6, 78, 59');
expect(healing.textStyle.textShadow).toContain('rgba(0, 0, 0');
});
it('uses mirrored stage anchors for player and opponent containers', () => {
expect(getMirroredStageEntityLeft('15%', 'player')).toBe('15%');
expect(getMirroredStageEntityLeft('15%', 'opponent')).toBe(`calc(100% - 15% - ${ENTITY_CONTAINER_REM}rem)`);

View File

@@ -1,5 +1,5 @@
import {motion} from 'motion/react';
import {type ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import {type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import {getCharacterById} from '../../data/characterPresets';
import {getFacingTowardPlayer, MONSTERS_BY_WORLD} from '../../data/hostileNpcs';
@@ -130,6 +130,45 @@ function getSceneTransitionMotionConfig(
};
}
export function getCombatFloatingNumberPresentation(isHealing: boolean): {
toneClass: string;
textStyle: CSSProperties;
} {
const textShadow = [
'0 1px 0 rgba(0, 0, 0, 0.98)',
'0 0 8px rgba(0, 0, 0, 0.92)',
'0 0 16px rgba(0, 0, 0, 0.72)',
].join(', ');
if (isHealing) {
return {
toneClass: [
'border-emerald-100/70',
'bg-emerald-950/70',
'text-emerald-50',
'shadow-[0_0_18px_rgba(52,211,153,0.55)]',
].join(' '),
textStyle: {
WebkitTextStroke: '1.45px rgba(6, 78, 59, 0.95)',
textShadow,
},
};
}
return {
toneClass: [
'border-rose-100/75',
'bg-rose-950/72',
'text-rose-50',
'shadow-[0_0_20px_rgba(248,113,113,0.68)]',
].join(' '),
textStyle: {
WebkitTextStroke: '1.55px rgba(127, 29, 29, 0.98)',
textShadow,
},
};
}
function CombatFloatingNumber({
event,
onDone,
@@ -139,23 +178,20 @@ function CombatFloatingNumber({
}) {
const isHealing = event.delta > 0;
const deltaText = `${isHealing ? '+' : ''}${event.delta}`;
const colorClass = isHealing ? 'text-emerald-200' : 'text-rose-200';
const glowClass = isHealing
? 'drop-shadow-[0_0_8px_rgba(52,211,153,0.9)]'
: 'drop-shadow-[0_0_8px_rgba(248,113,113,0.9)]';
const presentation = getCombatFloatingNumberPresentation(isHealing);
return (
<motion.div
key={event.id}
initial={{opacity: 0, y: 10, scale: 0.76}}
animate={{opacity: [0, 1, 1, 0], y: [10, -12, -31, -50], scale: [0.76, 1.22, 1, 0.9]}}
initial={{opacity: 0, y: 8, scale: 0.72}}
animate={{opacity: [0, 1, 1, 0], y: [8, -14, -36, -58], scale: [0.72, 1.18, 1.04, 0.92]}}
transition={{duration: 0.92, ease: 'easeOut'}}
onAnimationComplete={() => onDone(event.id)}
className={`pointer-events-none absolute -top-16 left-1/2 z-[14] -translate-x-1/2 text-lg font-black leading-none ${colorClass} ${glowClass}`}
className={`pointer-events-none absolute -top-[4.65rem] left-1/2 z-[38] flex min-w-[2.4rem] -translate-x-1/2 select-none items-center justify-center rounded-full border px-1.5 py-0.5 text-[1.45rem] font-black leading-none tracking-[-0.04em] sm:text-[1.6rem] ${presentation.toneClass}`}
data-testid={`combat-feedback-${event.targetKey}`}
aria-label={`战斗数值 ${deltaText}`}
>
<span className="[-webkit-text-stroke:1px_rgba(24,24,27,0.76)]">
<span style={presentation.textStyle}>
{deltaText}
</span>
</motion.div>