Commit
This commit is contained in:
339
pwa/src/components/game/CombatView.tsx
Normal file
339
pwa/src/components/game/CombatView.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import type { CombatState, CombatLogEntry, Profile, Equipment, PlayerState } from './types'
|
||||
|
||||
interface CombatViewProps {
|
||||
combatState: CombatState
|
||||
combatLog: CombatLogEntry[]
|
||||
profile: Profile | null
|
||||
playerState: PlayerState | null
|
||||
equipment: Equipment
|
||||
enemyName: string
|
||||
enemyImage: string
|
||||
enemyTurnMessage: string
|
||||
pvpTimeRemaining: number | null
|
||||
turnTimeRemaining: number | null
|
||||
onCombatAction: (action: string) => void
|
||||
onFlee: () => void
|
||||
onPvPAction: (action: string) => void
|
||||
onExitCombat: () => void
|
||||
onExitPvPCombat: () => void
|
||||
flashEnemy?: boolean
|
||||
buttonsDisabled?: boolean
|
||||
floatingTexts?: { id: number, text: string, x: number, y: number, type: 'damage-player' | 'damage-enemy' | 'damage-player-dealt' | 'heal' }[]
|
||||
}
|
||||
|
||||
function CombatView({
|
||||
combatState,
|
||||
combatLog,
|
||||
profile: _profile,
|
||||
playerState,
|
||||
enemyName,
|
||||
enemyImage,
|
||||
enemyTurnMessage,
|
||||
pvpTimeRemaining,
|
||||
turnTimeRemaining,
|
||||
onCombatAction,
|
||||
onPvPAction,
|
||||
onExitCombat,
|
||||
onExitPvPCombat,
|
||||
flashEnemy,
|
||||
buttonsDisabled,
|
||||
floatingTexts = []
|
||||
}: CombatViewProps) {
|
||||
return (
|
||||
<div className="combat-view">
|
||||
<div className="combat-header-inline">
|
||||
<h2>
|
||||
{combatState.is_pvp ? '⚔️ PvP Combat' : `⚔️ Combat - ${enemyName || combatState.combat?.npc_name || 'Enemy'}`}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{combatState.is_pvp ? (
|
||||
/* PvP Combat UI - Unified Layout */
|
||||
<div className="combat-content-wrapper">
|
||||
<div className="combat-enemy-display-inline">
|
||||
{/* Opponent Display (using same structure as PvE Enemy) */}
|
||||
<div className="combat-enemy-image-large">
|
||||
{floatingTexts.map(ft => (
|
||||
<div
|
||||
key={ft.id}
|
||||
className={`floating-text ${ft.type}`}
|
||||
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}>
|
||||
{ft.text}
|
||||
</div>
|
||||
))}
|
||||
{(() => {
|
||||
if (!combatState.pvp_combat) return null
|
||||
const opponent = combatState.pvp_combat.is_attacker ?
|
||||
combatState.pvp_combat.defender :
|
||||
combatState.pvp_combat.attacker
|
||||
|
||||
if (!opponent) return <div className="pvp-opponent-avatar">❓</div>
|
||||
// Use a default avatar if no image, or maybe the class image if available?
|
||||
// For now, let's use a placeholder or try to get it from profile if passed?
|
||||
// The opponent object has: username, level, hp, max_hp.
|
||||
// It might not have an image url.
|
||||
return (
|
||||
<div className="pvp-opponent-avatar" style={{ fontSize: '4rem', textAlign: 'center' }}>
|
||||
👤
|
||||
<div style={{ fontSize: '1rem', marginTop: '0.5rem' }}>{opponent.username} (Lv. {opponent.level})</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="combat-enemy-info-inline">
|
||||
{/* Opponent HP Bar */}
|
||||
{(() => {
|
||||
if (!combatState.pvp_combat) return null
|
||||
const opponent = combatState.pvp_combat.is_attacker ?
|
||||
combatState.pvp_combat.defender :
|
||||
combatState.pvp_combat.attacker
|
||||
|
||||
if (!opponent) return null
|
||||
|
||||
return (
|
||||
<div className="combat-hp-bar-container-inline enemy-hp-bar">
|
||||
<div className="combat-hp-bar-inline">
|
||||
<div className="combat-stat-label-inline">
|
||||
{opponent.username}: {opponent.hp} / {opponent.max_hp}
|
||||
</div>
|
||||
<div
|
||||
className="combat-hp-fill-inline"
|
||||
style={{
|
||||
width: `${(opponent.hp / opponent.max_hp) * 100}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Player HP Bar */}
|
||||
{(() => {
|
||||
if (!combatState.pvp_combat) return null
|
||||
const you = combatState.pvp_combat.is_attacker ?
|
||||
combatState.pvp_combat.attacker :
|
||||
combatState.pvp_combat.defender
|
||||
|
||||
if (!you) return null
|
||||
|
||||
return (
|
||||
<div className="combat-hp-bar-container-inline player-hp-bar" style={{ marginTop: '0.75rem' }}>
|
||||
<div className="combat-hp-bar-inline" style={{ background: 'rgba(255, 107, 107, 0.2)' }}>
|
||||
<div className="combat-stat-label-inline" style={{ color: '#ff6b6b' }}>
|
||||
You: {you.hp} / {you.max_hp}
|
||||
</div>
|
||||
<div
|
||||
className="combat-hp-fill-inline"
|
||||
style={{
|
||||
width: `${(you.hp / you.max_hp) * 100}%`,
|
||||
background: 'linear-gradient(90deg, #f44336, #ff6b6b)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="combat-turn-indicator-inline">
|
||||
{combatState.pvp_combat.combat_over ? (
|
||||
<span className={combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? "your-turn" : "enemy-turn"}>
|
||||
{combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? "🏃 Combat Ended" : "💀 Combat Over"}
|
||||
</span>
|
||||
) : combatState.pvp_combat.your_turn ? (
|
||||
<span className="your-turn">✅ Your Turn ({pvpTimeRemaining ?? combatState.pvp_combat.time_remaining}s)</span>
|
||||
) : (
|
||||
<span className="enemy-turn">⏳ Opponent's Turn ({pvpTimeRemaining ?? combatState.pvp_combat.time_remaining}s)</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="combat-actions-inline">
|
||||
{!combatState.pvp_combat.combat_over ? (
|
||||
<>
|
||||
<button
|
||||
className="combat-action-btn attack-btn"
|
||||
onClick={() => onPvPAction('attack')}
|
||||
disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
|
||||
>
|
||||
⚔️ Attack
|
||||
</button>
|
||||
<button
|
||||
className="combat-action-btn flee-btn"
|
||||
onClick={() => onPvPAction('flee')}
|
||||
disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
|
||||
>
|
||||
🏃 Flee
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="combat-action-btn exit-btn"
|
||||
onClick={onExitPvPCombat}
|
||||
>
|
||||
{combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? '✅ Continue' : '💀 Return'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Combat Log */}
|
||||
<div className="combat-log-wrapper">
|
||||
<h3 className="combat-log-title">Combat Log</h3>
|
||||
<div className="combat-log-inline">
|
||||
<div className="log-entries">
|
||||
{combatLog.map((entry: any, i: number) => (
|
||||
<div key={i} className={`log-entry ${entry.isPlayer ? 'player-log' : 'enemy-log'}`}>
|
||||
<span className="log-time">[{entry.time}]</span>
|
||||
<span className="log-message">{entry.message}</span>
|
||||
</div>
|
||||
))}
|
||||
{combatLog.length === 0 && <div className="log-entry"><span className="log-message">PvP Combat started...</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* PvE Combat UI */
|
||||
<>
|
||||
<div className="combat-content-wrapper">
|
||||
<div className="combat-enemy-display-inline">
|
||||
{/* Intent Bubble - Moved here to avoid overflow:hidden clipping */}
|
||||
{combatState.combat?.npc_intent && !combatState.combat_over && (
|
||||
<div className={`intent-bubble intent-${combatState.combat.npc_intent}`}>
|
||||
<span className="intent-icon">
|
||||
{combatState.combat.npc_intent === 'attack' ? '⚔️' :
|
||||
combatState.combat.npc_intent === 'defend' ? '🛡️' :
|
||||
combatState.combat.npc_intent === 'special' ? '🔥' : '❓'}
|
||||
</span>
|
||||
<span className="intent-desc">{combatState.combat.npc_intent}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="combat-enemy-image-large">
|
||||
{floatingTexts.map(ft => (
|
||||
<div
|
||||
key={ft.id}
|
||||
className={`floating-text ${ft.type}`}
|
||||
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}>
|
||||
{ft.text}
|
||||
</div>
|
||||
))}
|
||||
<img
|
||||
src={enemyImage || combatState.combat?.npc_image || combatState.combat_image}
|
||||
alt={enemyName || combatState.combat?.npc_name || 'Enemy'}
|
||||
className={`${flashEnemy ? 'flash-hit' : ''
|
||||
} ${combatState.combat_over && combatState.player_won ? 'enemy-dead' : ''
|
||||
} ${combatState.combat_over && combatState.player_fled ? 'enemy-fled' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="combat-enemy-info-inline">
|
||||
<div className="combat-hp-bar-container-inline enemy-hp-bar">
|
||||
<div className="combat-hp-bar-inline">
|
||||
<div className="combat-stat-label-inline">
|
||||
Enemy HP: {combatState.combat?.npc_hp || 0} / {combatState.combat?.npc_max_hp || 100}
|
||||
</div>
|
||||
<div
|
||||
className="combat-hp-fill-inline"
|
||||
style={{
|
||||
width: `${((combatState.combat?.npc_hp || 0) / (combatState.combat?.npc_max_hp || 100)) * 100}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{playerState && (
|
||||
<div className="combat-hp-bar-container-inline player-hp-bar" style={{ marginTop: '0.75rem' }}>
|
||||
<div className="combat-hp-bar-inline" style={{ background: 'rgba(255, 107, 107, 0.2)' }}>
|
||||
<div className="combat-stat-label-inline" style={{ color: '#ff6b6b' }}>
|
||||
Your HP: {playerState.health} / {playerState.max_health}
|
||||
</div>
|
||||
<div
|
||||
className="combat-hp-fill-inline"
|
||||
style={{
|
||||
width: `${(playerState.health / playerState.max_health) * 100}%`,
|
||||
background: 'linear-gradient(90deg, #f44336, #ff6b6b)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`combat-turn-indicator-inline ${enemyTurnMessage ? 'enemy-turn-message' : ''}`}>
|
||||
{!combatState.combat_over ? (
|
||||
enemyTurnMessage ? (
|
||||
<span className="enemy-turn">🗡️ Enemy's turn...</span>
|
||||
) : combatState.combat?.turn === 'player' ? (
|
||||
<>
|
||||
<span className="your-turn">✅ Your Turn</span>
|
||||
{turnTimeRemaining !== null && (
|
||||
<span className="turn-timer" style={{ marginLeft: '1rem', fontSize: '0.9em', opacity: 0.8 }}>
|
||||
⏱️ {Math.floor(turnTimeRemaining / 60)}:{String(Math.floor(turnTimeRemaining % 60)).padStart(2, '0')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="enemy-turn">⚠️ Enemy Turn</span>
|
||||
)
|
||||
) : (
|
||||
<span className={combatState.player_won ? "your-turn" : combatState.player_fled ? "your-turn" : "enemy-turn"}>
|
||||
{combatState.player_won ? "✅ Victory!" : combatState.player_fled ? "🏃 Escaped!" : "💀 Defeated"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PvE Combat Actions */}
|
||||
|
||||
<div className="combat-actions-inline">
|
||||
{!combatState.combat_over ? (
|
||||
<>
|
||||
<button
|
||||
className="combat-action-btn attack-btn"
|
||||
onClick={() => onCombatAction('attack')}
|
||||
disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
|
||||
>
|
||||
⚔️ Attack
|
||||
</button>
|
||||
<button
|
||||
className="combat-action-btn flee-btn"
|
||||
onClick={() => onCombatAction('flee')}
|
||||
disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
|
||||
>
|
||||
🏃 Flee
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="combat-action-btn exit-btn"
|
||||
onClick={onExitCombat}
|
||||
>
|
||||
{combatState.player_won ? '✅ Continue' : combatState.player_fled ? '✅ Continue' : '💀 Return'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Combat Log */}
|
||||
<div className="combat-log-wrapper">
|
||||
<h3 className="combat-log-title">Combat Log</h3>
|
||||
<div className="combat-log-inline">
|
||||
<div className="log-entries">
|
||||
{combatLog.map((entry: any, i: number) => (
|
||||
<div key={i} className={`log-entry ${entry.isPlayer ? 'player-log' : 'enemy-log'}`}>
|
||||
<span className="log-time">[{entry.time}]</span>
|
||||
<span className="log-message">{entry.message}</span>
|
||||
</div>
|
||||
))}
|
||||
{combatLog.length === 0 && <div className="log-entry"><span className="log-message">Combat started...</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CombatView
|
||||
Reference in New Issue
Block a user