You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
5.0 KiB
TypeScript
209 lines
5.0 KiB
TypeScript
import { useCallback, type CSSProperties } from 'react';
|
|
import type { NPCData } from '../game/types';
|
|
|
|
// ---- Types ----
|
|
|
|
interface NPCInteractionProps {
|
|
npc: NPCData;
|
|
heroGold: number;
|
|
potionCost: number;
|
|
healCost: number;
|
|
onViewQuests: (npc: NPCData) => void;
|
|
onBuyPotion: (npc: NPCData) => void;
|
|
onHeal: (npc: NPCData) => void;
|
|
onDismiss: () => void;
|
|
}
|
|
|
|
// ---- Styles ----
|
|
|
|
const panelStyle: CSSProperties = {
|
|
position: 'absolute',
|
|
bottom: 130,
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
minWidth: 220,
|
|
maxWidth: 320,
|
|
backgroundColor: 'rgba(15, 15, 25, 0.94)',
|
|
border: '1px solid rgba(255, 215, 0, 0.3)',
|
|
borderRadius: 12,
|
|
padding: '10px 14px 12px',
|
|
zIndex: 120,
|
|
pointerEvents: 'auto',
|
|
animation: 'npc-interact-fade-in 0.25s ease-out',
|
|
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)',
|
|
};
|
|
|
|
const headerRow: CSSProperties = {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
marginBottom: 8,
|
|
};
|
|
|
|
const npcIconStyle: CSSProperties = {
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 14,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: 14,
|
|
fontWeight: 700,
|
|
};
|
|
|
|
const npcNameStyle: CSSProperties = {
|
|
fontSize: 13,
|
|
fontWeight: 700,
|
|
color: '#e8e8e8',
|
|
flex: 1,
|
|
};
|
|
|
|
const npcTypeStyle: CSSProperties = {
|
|
fontSize: 9,
|
|
fontWeight: 600,
|
|
color: '#888',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
};
|
|
|
|
const actionBtnStyle: CSSProperties = {
|
|
width: '100%',
|
|
padding: '8px 12px',
|
|
border: 'none',
|
|
borderRadius: 8,
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
marginBottom: 4,
|
|
transition: 'background-color 150ms ease',
|
|
textAlign: 'center',
|
|
};
|
|
|
|
// ---- NPC appearance ----
|
|
|
|
function npcColor(type: string): { bg: string; icon: string; text: string } {
|
|
switch (type) {
|
|
case 'quest_giver':
|
|
return { bg: 'rgba(218, 165, 32, 0.2)', icon: '!', text: 'Quest Giver' };
|
|
case 'merchant':
|
|
return { bg: 'rgba(68, 170, 85, 0.2)', icon: '$', text: 'Merchant' };
|
|
case 'healer':
|
|
return { bg: 'rgba(220, 80, 80, 0.2)', icon: '+', text: 'Healer' };
|
|
default:
|
|
return { bg: 'rgba(136, 136, 170, 0.2)', icon: '?', text: 'NPC' };
|
|
}
|
|
}
|
|
|
|
// ---- Component ----
|
|
|
|
export function NPCInteraction({
|
|
npc,
|
|
heroGold,
|
|
potionCost,
|
|
healCost,
|
|
onViewQuests,
|
|
onBuyPotion,
|
|
onHeal,
|
|
onDismiss,
|
|
}: NPCInteractionProps) {
|
|
const info = npcColor(npc.type);
|
|
|
|
const handleAction = useCallback(() => {
|
|
switch (npc.type) {
|
|
case 'quest_giver':
|
|
onViewQuests(npc);
|
|
break;
|
|
case 'merchant':
|
|
onBuyPotion(npc);
|
|
break;
|
|
case 'healer':
|
|
onHeal(npc);
|
|
break;
|
|
}
|
|
}, [npc, onViewQuests, onBuyPotion, onHeal]);
|
|
|
|
const actionLabel = (() => {
|
|
switch (npc.type) {
|
|
case 'quest_giver':
|
|
return 'View Quests';
|
|
case 'merchant':
|
|
return `Buy Potion (${potionCost}g)`;
|
|
case 'healer':
|
|
return `Heal to Full (${healCost}g)`;
|
|
default:
|
|
return 'Talk';
|
|
}
|
|
})();
|
|
|
|
const canAfford =
|
|
npc.type === 'quest_giver' ||
|
|
(npc.type === 'merchant' && heroGold >= potionCost) ||
|
|
(npc.type === 'healer' && heroGold >= healCost);
|
|
|
|
return (
|
|
<>
|
|
<style>{`
|
|
@keyframes npc-interact-fade-in {
|
|
0% { opacity: 0; transform: translateX(-50%) translateY(8px); }
|
|
100% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
}
|
|
`}</style>
|
|
<div style={panelStyle}>
|
|
<div style={headerRow}>
|
|
<div
|
|
style={{
|
|
...npcIconStyle,
|
|
backgroundColor: info.bg,
|
|
color: npc.type === 'quest_giver' ? '#ffd700' :
|
|
npc.type === 'merchant' ? '#88dd88' :
|
|
npc.type === 'healer' ? '#ff6666' : '#aaa',
|
|
}}
|
|
>
|
|
{info.icon}
|
|
</div>
|
|
<div>
|
|
<div style={npcNameStyle}>{npc.name}</div>
|
|
<div style={npcTypeStyle}>{info.text}</div>
|
|
</div>
|
|
<button
|
|
onClick={onDismiss}
|
|
style={{
|
|
background: 'none',
|
|
border: 'none',
|
|
color: '#666',
|
|
fontSize: 16,
|
|
cursor: 'pointer',
|
|
padding: '2px 6px',
|
|
lineHeight: 1,
|
|
}}
|
|
>
|
|
{'\u2715'}
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
style={{
|
|
...actionBtnStyle,
|
|
backgroundColor: canAfford
|
|
? (npc.type === 'quest_giver' ? 'rgba(68, 170, 255, 0.2)' :
|
|
npc.type === 'merchant' ? 'rgba(68, 200, 68, 0.2)' :
|
|
'rgba(200, 68, 68, 0.2)')
|
|
: 'rgba(100, 100, 100, 0.15)',
|
|
color: canAfford
|
|
? (npc.type === 'quest_giver' ? '#66bbff' :
|
|
npc.type === 'merchant' ? '#88dd88' :
|
|
'#ff8888')
|
|
: '#666',
|
|
opacity: canAfford ? 1 : 0.5,
|
|
cursor: canAfford ? 'pointer' : 'default',
|
|
}}
|
|
onClick={canAfford ? handleAction : undefined}
|
|
disabled={!canAfford}
|
|
>
|
|
{actionLabel}
|
|
</button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|