Push
This commit is contained in:
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../hooks/useAuth'
|
||||
import { Character } from '../services/api'
|
||||
import './CharacterSelection.css'
|
||||
import { GameTooltip } from './common/GameTooltip'
|
||||
|
||||
function CharacterSelection() {
|
||||
const { characters, account, selectCharacter, deleteCharacter, logout } = useAuth()
|
||||
@@ -14,7 +15,7 @@ function CharacterSelection() {
|
||||
const handleSelectCharacter = async (characterId: number) => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
|
||||
try {
|
||||
await selectCharacter(characterId)
|
||||
navigate('/game')
|
||||
@@ -32,7 +33,7 @@ function CharacterSelection() {
|
||||
|
||||
setDeletingId(characterId)
|
||||
setError('')
|
||||
|
||||
|
||||
try {
|
||||
await deleteCharacter(characterId)
|
||||
} catch (err: any) {
|
||||
@@ -102,12 +103,12 @@ function CharacterSelection() {
|
||||
)
|
||||
}
|
||||
|
||||
function CharacterCard({
|
||||
character,
|
||||
onSelect,
|
||||
onDelete,
|
||||
loading
|
||||
}: {
|
||||
function CharacterCard({
|
||||
character,
|
||||
onSelect,
|
||||
onDelete,
|
||||
loading
|
||||
}: {
|
||||
character: Character
|
||||
onSelect: () => void
|
||||
onDelete: () => void
|
||||
@@ -135,11 +136,21 @@ function CharacterCard({
|
||||
<span className="stat">Level {character.level}</span>
|
||||
<span className="stat">HP: {character.hp}/{character.max_hp}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="character-attributes">
|
||||
<span title="Strength">💪 {character.strength}</span>
|
||||
<span title="Agility">⚡ {character.agility}</span>
|
||||
<span title="Endurance">🛡️ {character.endurance}</span>
|
||||
<span title="Intellect">🧠 {character.intellect}</span>
|
||||
<GameTooltip content="Strength">
|
||||
<span className="stat-icon">💪 {character.strength}</span>
|
||||
</GameTooltip>
|
||||
<GameTooltip content="Agility">
|
||||
<span>⚡ {character.agility}</span>
|
||||
</GameTooltip>
|
||||
<GameTooltip content="Endurance">
|
||||
<span>🛡️ {character.endurance}</span>
|
||||
</GameTooltip>
|
||||
<GameTooltip content="Intellect">
|
||||
<span>🧠 {character.intellect}</span>
|
||||
</GameTooltip>
|
||||
</div>
|
||||
<p className="character-meta">
|
||||
Last played: {formatDate(character.last_played_at)}
|
||||
@@ -147,15 +158,15 @@ function CharacterCard({
|
||||
</div>
|
||||
|
||||
<div className="character-actions">
|
||||
<button
|
||||
className="button-primary"
|
||||
<button
|
||||
className="button-primary"
|
||||
onClick={onSelect}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Play'}
|
||||
</button>
|
||||
<button
|
||||
className="button-danger"
|
||||
<button
|
||||
className="button-danger"
|
||||
onClick={onDelete}
|
||||
disabled={loading}
|
||||
>
|
||||
|
||||
@@ -7,9 +7,10 @@ html {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #eee;
|
||||
background: var(--game-bg-app);
|
||||
color: var(--game-text-primary);
|
||||
position: relative;
|
||||
font-family: var(--game-font-main);
|
||||
}
|
||||
|
||||
/* Death Overlay */
|
||||
@@ -95,23 +96,23 @@ html {
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
background-color: rgba(20, 20, 25, 0.95);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background-color: var(--game-bg-panel);
|
||||
border-bottom: 1px solid var(--game-border-color);
|
||||
backdrop-filter: blur(10px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(45deg, #ff6b6b, #ffa502);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--game-text-highlight);
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 0 10px rgba(234, 113, 66, 0.3);
|
||||
}
|
||||
|
||||
/* Player Count Badge */
|
||||
@@ -172,28 +173,28 @@ html {
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--game-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border-color: rgba(107, 185, 240, 0.5);
|
||||
transform: translateY(-2px);
|
||||
color: var(--game-text-primary);
|
||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: rgba(107, 185, 240, 0.2);
|
||||
border-color: #6bb9f0;
|
||||
color: #6bb9f0;
|
||||
color: var(--game-color-primary);
|
||||
border-bottom: 2px solid var(--game-color-primary);
|
||||
background: linear-gradient(to top, rgba(234, 113, 66, 0.1), transparent);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@@ -231,9 +232,10 @@ html {
|
||||
.game-stats-bar {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1rem 2rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 0.8rem 2rem;
|
||||
background: var(--game-bg-dark);
|
||||
border-bottom: 1px solid var(--game-border-color);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-bar-container {
|
||||
@@ -263,28 +265,27 @@ html {
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: var(--game-radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--game-border-color);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.5s ease;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill.health {
|
||||
background: linear-gradient(90deg, #dc3545 0%, #ff6b6b 100%);
|
||||
box-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
|
||||
background: var(--game-gradient-health);
|
||||
box-shadow: 0 0 10px rgba(196, 92, 92, 0.3);
|
||||
}
|
||||
|
||||
.progress-fill.stamina {
|
||||
background: linear-gradient(90deg, #ffc107 0%, #ffeb3b 100%);
|
||||
box-shadow: 0 0 10px rgba(255, 235, 59, 0.5);
|
||||
background: var(--game-gradient-stamina);
|
||||
box-shadow: 0 0 10px rgba(226, 180, 103, 0.3);
|
||||
}
|
||||
|
||||
/* Legacy stat styles for backwards compatibility */
|
||||
@@ -302,9 +303,10 @@ html {
|
||||
|
||||
.game-tabs {
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 2px solid rgba(255, 107, 107, 0.3);
|
||||
background: var(--game-bg-panel);
|
||||
border-bottom: 2px solid var(--game-border-color);
|
||||
overflow-x: auto;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -312,22 +314,25 @@ html {
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #aaa;
|
||||
color: var(--game-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--game-text-primary);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
color: #ff6b6b;
|
||||
border-bottom: 3px solid #ff6b6b;
|
||||
background: linear-gradient(to top, rgba(234, 113, 66, 0.1), transparent);
|
||||
color: var(--game-color-primary);
|
||||
border-bottom: 3px solid var(--game-color-primary);
|
||||
}
|
||||
|
||||
.game-main {
|
||||
@@ -385,70 +390,76 @@ html {
|
||||
}
|
||||
|
||||
.location-info {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: var(--game-bg-panel);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--game-radius-md);
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
border: 1px solid var(--game-border-color);
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.location-info h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #ff6b6b;
|
||||
color: var(--game-text-highlight);
|
||||
font-size: 1.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.danger-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1.2rem;
|
||||
border-radius: 24px;
|
||||
font-size: 1rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: var(--game-radius-sm);
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.danger-safe {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 2px solid #4caf50;
|
||||
background: rgba(139, 179, 128, 0.15);
|
||||
color: var(--game-danger-safe);
|
||||
border-color: var(--game-danger-safe);
|
||||
}
|
||||
|
||||
.danger-1 {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
color: #ffc107;
|
||||
border: 2px solid #ffc107;
|
||||
background: rgba(226, 180, 103, 0.15);
|
||||
color: var(--game-danger-low);
|
||||
border-color: var(--game-danger-low);
|
||||
}
|
||||
|
||||
.danger-2 {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
border: 2px solid #ff9800;
|
||||
background: rgba(234, 113, 66, 0.15);
|
||||
color: var(--game-danger-med);
|
||||
border-color: var(--game-danger-med);
|
||||
}
|
||||
|
||||
.danger-3 {
|
||||
background: rgba(255, 87, 34, 0.2);
|
||||
color: #ff5722;
|
||||
border: 2px solid #ff5722;
|
||||
background: rgba(196, 92, 92, 0.15);
|
||||
color: var(--game-danger-high);
|
||||
border-color: var(--game-danger-high);
|
||||
}
|
||||
|
||||
.danger-4 {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 2px solid #f44336;
|
||||
background: rgba(196, 92, 92, 0.25);
|
||||
color: var(--game-danger-high);
|
||||
border-color: var(--game-danger-high);
|
||||
box-shadow: 0 0 8px rgba(196, 92, 92, 0.3);
|
||||
}
|
||||
|
||||
.danger-5 {
|
||||
background: rgba(156, 39, 176, 0.2);
|
||||
color: #9c27b0;
|
||||
border: 2px solid #9c27b0;
|
||||
background: rgba(163, 62, 62, 0.25);
|
||||
color: var(--game-danger-extreme);
|
||||
border-color: var(--game-danger-extreme);
|
||||
box-shadow: 0 0 12px rgba(163, 62, 62, 0.4);
|
||||
}
|
||||
|
||||
.location-tags {
|
||||
@@ -721,16 +732,19 @@ html {
|
||||
}
|
||||
|
||||
.movement-controls {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: var(--game-bg-panel);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(255, 107, 107, 0.3);
|
||||
border-radius: var(--game-radius-md);
|
||||
border: 1px solid var(--game-border-color);
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.movement-controls h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #ff6b6b;
|
||||
color: var(--game-text-highlight);
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 8-Direction Compass Grid */
|
||||
@@ -746,18 +760,18 @@ html {
|
||||
.compass-btn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 2px solid rgba(255, 107, 107, 0.3);
|
||||
background: linear-gradient(135deg, rgba(255, 107, 107, 0.2) 0%, rgba(255, 107, 107, 0.3) 100%);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--game-border-color);
|
||||
background: linear-gradient(135deg, rgba(80, 80, 90, 0.3) 0%, rgba(40, 40, 50, 0.5) 100%);
|
||||
color: var(--game-text-primary);
|
||||
border-radius: var(--game-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: var(--game-shadow-card);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -769,7 +783,7 @@ html {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 100%);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -783,7 +797,7 @@ html {
|
||||
.compass-cost {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: #ffc107;
|
||||
color: var(--game-color-warning);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
display: block;
|
||||
line-height: 1;
|
||||
@@ -791,10 +805,10 @@ html {
|
||||
}
|
||||
|
||||
.compass-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, rgba(255, 107, 107, 0.4) 0%, rgba(255, 107, 107, 0.5) 100%);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
|
||||
border-color: rgba(255, 107, 107, 0.6);
|
||||
background: linear-gradient(135deg, rgba(234, 113, 66, 0.2) 0%, rgba(234, 113, 66, 0.3) 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
border-color: var(--game-color-primary);
|
||||
}
|
||||
|
||||
.compass-btn:active:not(:disabled) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import LanguageSelector from './LanguageSelector'
|
||||
import './Game.css'
|
||||
|
||||
import { GameTooltip } from './common/GameTooltip'
|
||||
|
||||
interface GameHeaderProps {
|
||||
className?: string
|
||||
}
|
||||
@@ -77,10 +79,12 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
|
||||
</nav>
|
||||
<div className="user-info">
|
||||
<LanguageSelector />
|
||||
<div className="player-count-badge" title={t('game.onlineCount', { count: playerCount })}>
|
||||
<span className="status-dot"></span>
|
||||
<span className="count-text">{t('game.onlineCount', { count: playerCount })}</span>
|
||||
</div>
|
||||
<GameTooltip content={t('game.onlineCount', { count: playerCount })}>
|
||||
<div className="player-count-badge">
|
||||
<span className="status-dot"></span>
|
||||
<span className="count-text">{t('game.onlineCount', { count: playerCount })}</span>
|
||||
</div>
|
||||
</GameTooltip>
|
||||
<button
|
||||
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
|
||||
className={`username-link ${isOnOwnProfile ? 'active' : ''}`}
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
/* Header styles removed - using game-header from Game.css */
|
||||
|
||||
/* Loading and error states */
|
||||
.game-main .profile-loading,
|
||||
.game-main .profile-loading,
|
||||
.game-main .profile-error {
|
||||
max-width: 600px;
|
||||
margin: 4rem auto;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: var(--game-bg-panel);
|
||||
padding: 3rem;
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(107, 185, 240, 0.3);
|
||||
border-radius: var(--game-radius-md);
|
||||
border: 2px solid var(--game-border-color);
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.game-main .profile-error button {
|
||||
@@ -36,14 +37,15 @@
|
||||
}
|
||||
|
||||
.profile-info-card {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 2px solid rgba(107, 185, 240, 0.3);
|
||||
border-radius: 12px;
|
||||
background: var(--game-bg-panel);
|
||||
border: 2px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
@@ -65,12 +67,12 @@
|
||||
.profile-name {
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #6bb9f0;
|
||||
color: var(--game-text-highlight);
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
font-size: 1rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: var(--game-text-secondary);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
@@ -121,17 +123,18 @@
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 2px solid rgba(107, 185, 240, 0.3);
|
||||
border-radius: 12px;
|
||||
background: var(--game-bg-panel);
|
||||
border: 2px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.3rem;
|
||||
margin: 0 0 1rem 0;
|
||||
color: #6bb9f0;
|
||||
border-bottom: 2px solid rgba(107, 185, 240, 0.3);
|
||||
color: var(--game-color-primary);
|
||||
border-bottom: 2px solid var(--game-border-color);
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -148,7 +151,7 @@
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: var(--game-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
@@ -156,7 +159,7 @@
|
||||
.stat-value {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: #fff;
|
||||
color: var(--game-text-primary);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
@@ -182,6 +185,7 @@
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
/* Remove tab bar spacing for profile page */
|
||||
.game-main {
|
||||
margin-bottom: 0 !important;
|
||||
@@ -190,7 +194,8 @@
|
||||
.game-main .profile-container {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1rem;
|
||||
padding-top: 4rem; /* Space for hamburger button */
|
||||
padding-top: 4rem;
|
||||
/* Space for hamburger button */
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -202,4 +207,4 @@
|
||||
.profile-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
104
pwa/src/components/common/GameTooltip.tsx
Normal file
104
pwa/src/components/common/GameTooltip.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState, useRef, ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
interface GameTooltipProps {
|
||||
content: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string; // Class for the wrapper
|
||||
}
|
||||
|
||||
/**
|
||||
* GameTooltip
|
||||
*
|
||||
* Wraps an element and displays a custom, instant game-style tooltip on hover.
|
||||
* Uses React Portal to render outside the DOM hierarchy to avoid overflow/z-index issues.
|
||||
* Follows the mouse cursor.
|
||||
*/
|
||||
export const GameTooltip: React.FC<GameTooltipProps> = ({ content, children, className = '' }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updatePosition = (e: React.MouseEvent) => {
|
||||
// Offset from cursor
|
||||
const offsetX = 15;
|
||||
const offsetY = 15;
|
||||
|
||||
// Check viewport boundaries to prevent overflow
|
||||
let x = e.clientX + offsetX;
|
||||
let y = e.clientY + offsetY;
|
||||
|
||||
// Simple boundary check (can be expanded if needed)
|
||||
if (tooltipRef.current) {
|
||||
const rect = tooltipRef.current.getBoundingClientRect();
|
||||
if (x + rect.width > window.innerWidth) {
|
||||
x = e.clientX - rect.width - 5;
|
||||
}
|
||||
if (y + rect.height > window.innerHeight) {
|
||||
y = e.clientY - rect.height - 5;
|
||||
}
|
||||
}
|
||||
|
||||
setPosition({ x, y });
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent) => {
|
||||
setIsVisible(true);
|
||||
updatePosition(e);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
updatePosition(e);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
// Render the tooltip portal
|
||||
const tooltip = isVisible && content ? (
|
||||
createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none', // Ensure mouse doesn't get stuck on the tooltip itself
|
||||
maxWidth: '300px'
|
||||
}}
|
||||
className="game-tooltip-content"
|
||||
>
|
||||
<div style={{
|
||||
background: 'var(--game-bg-tooltip, #151515)',
|
||||
border: '1px solid var(--game-border-color, #333)',
|
||||
borderRadius: 'var(--game-radius-sm, 4px)',
|
||||
padding: '0.5rem 0.8rem',
|
||||
boxShadow: 'var(--game-shadow-tooltip, 0 4px 12px rgba(0,0,0,0.5))',
|
||||
color: 'var(--game-text-primary, #ddd)',
|
||||
fontSize: '0.85rem',
|
||||
backdropFilter: 'blur(4px)'
|
||||
}}>
|
||||
{content}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`game-tooltip-wrapper ${className}`}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ display: 'contents' }} // Use contents so the wrapper doesn't affect layout
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{tooltip}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
/* Weight and Volume Progress Bars */
|
||||
.sidebar-progress-fill.weight {
|
||||
background: linear-gradient(90deg, #ff9800, #f57c00);
|
||||
background: var(--game-gradient-health);
|
||||
/* Using health/red-orange for weight/load */
|
||||
}
|
||||
|
||||
.sidebar-progress-fill.volume {
|
||||
background: linear-gradient(90deg, #9c27b0, #7b1fa2);
|
||||
background: var(--game-gradient-stamina);
|
||||
/* Using stamina/yellow-gold for volume */
|
||||
}
|
||||
|
||||
/* Inventory Tab - Full View */
|
||||
@@ -34,6 +36,7 @@
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* --- Redesigned Inventory Modal --- */
|
||||
/* --- Redesigned Inventory Modal --- */
|
||||
.inventory-modal-redesign {
|
||||
display: flex;
|
||||
@@ -41,14 +44,13 @@
|
||||
height: 85vh;
|
||||
width: 95vw;
|
||||
max-width: 1400px;
|
||||
/* Match Workbench width */
|
||||
background: linear-gradient(135deg, #1e2a38 0%, #121820 100%);
|
||||
border: 1px solid #3a4b5c;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.8);
|
||||
background: var(--game-bg-modal);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-lg);
|
||||
box-shadow: var(--game-shadow-modal);
|
||||
overflow: hidden;
|
||||
color: #e0e6ed;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
color: var(--game-text-primary);
|
||||
font-family: var(--game-font-main);
|
||||
}
|
||||
|
||||
/* Top Bar */
|
||||
@@ -57,8 +59,8 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
background: var(--game-bg-panel);
|
||||
border-bottom: 1px solid var(--game-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -94,23 +96,24 @@
|
||||
|
||||
.metric-bar {
|
||||
height: 8px;
|
||||
background: #2d3748;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: var(--game-radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--game-border-color);
|
||||
}
|
||||
|
||||
.metric-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--game-radius-sm);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.metric-fill.weight {
|
||||
background: linear-gradient(90deg, #48bb78, #38a169);
|
||||
background: var(--game-gradient-health);
|
||||
}
|
||||
|
||||
.metric-fill.volume {
|
||||
background: linear-gradient(90deg, #4299e1, #3182ce);
|
||||
background: var(--game-gradient-stamina);
|
||||
}
|
||||
|
||||
.inventory-backpack-info {
|
||||
@@ -168,11 +171,12 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar Filters */
|
||||
/* Sidebar Filters */
|
||||
.inventory-sidebar-filters {
|
||||
width: 220px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-right: 1px solid #3a4b5c;
|
||||
background: var(--game-bg-panel);
|
||||
border-right: 1px solid var(--game-border-color);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -231,10 +235,11 @@
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #3a4b5c;
|
||||
border-radius: 8px;
|
||||
background: var(--game-bg-input);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--game-text-primary);
|
||||
}
|
||||
|
||||
.inventory-search-bar input {
|
||||
@@ -255,19 +260,20 @@
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Compact Item Card */
|
||||
/* Compact Item Card */
|
||||
.inventory-item-card.compact {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: rgba(26, 32, 44, 0.8);
|
||||
border: 1px solid #2d3748;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--game-bg-card);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
padding: 0.75rem;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 0.75rem;
|
||||
/* Add separation between cards */
|
||||
box-shadow: var(--game-shadow-sm);
|
||||
}
|
||||
|
||||
.inventory-item-card.compact:hover {
|
||||
@@ -311,13 +317,14 @@
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
background: #2d3748;
|
||||
border: 1px solid #4a5568;
|
||||
color: #fff;
|
||||
background: var(--game-bg-panel);
|
||||
border: 1px solid var(--game-border-color);
|
||||
color: var(--game-text-primary);
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--game-radius-sm);
|
||||
font-weight: bold;
|
||||
box-shadow: var(--game-shadow-sm);
|
||||
}
|
||||
|
||||
.item-info-section {
|
||||
@@ -705,13 +712,13 @@
|
||||
}
|
||||
|
||||
.action-btn.unequip {
|
||||
background: rgba(237, 137, 54, 0.2);
|
||||
color: #ed8936;
|
||||
border: 1px solid rgba(237, 137, 54, 0.4);
|
||||
background: rgba(234, 113, 66, 0.1);
|
||||
color: var(--game-color-primary);
|
||||
border: 1px solid var(--game-color-primary);
|
||||
}
|
||||
|
||||
.action-btn.unequip:hover {
|
||||
background: rgba(237, 137, 54, 0.3);
|
||||
background: rgba(234, 113, 66, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getAssetPath } from '../../utils/assetPath'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
import './InventoryModal.css'
|
||||
import { EffectBadge } from './EffectBadge'
|
||||
import { GameTooltip } from '../common/GameTooltip'
|
||||
|
||||
interface InventoryModalProps {
|
||||
playerState: PlayerState
|
||||
@@ -285,19 +286,20 @@ function InventoryModal({
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`action-btn use ${isEffectActive ? 'disabled' : ''}`}
|
||||
disabled={isEffectActive}
|
||||
title={isEffectActive ? t('game.effectAlreadyActive') : ''}
|
||||
onClick={() => {
|
||||
if (!isEffectActive) {
|
||||
playSfx('/audio/sfx/use.wav')
|
||||
onUseItem(item.item_id, item.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('game.use')}
|
||||
</button>
|
||||
<GameTooltip content={isEffectActive ? t('game.effectAlreadyActive') : ''}>
|
||||
<button
|
||||
className={`action-btn use ${isEffectActive ? 'disabled' : ''}`}
|
||||
disabled={isEffectActive}
|
||||
onClick={() => {
|
||||
if (!isEffectActive) {
|
||||
playSfx('/audio/sfx/use.wav')
|
||||
onUseItem(item.item_id, item.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('game.use')}
|
||||
</button>
|
||||
</GameTooltip>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from '
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAudio } from '../../contexts/AudioContext'
|
||||
import Workbench from './Workbench'
|
||||
import { GameTooltip } from '../common/GameTooltip'
|
||||
import { getAssetPath } from '../../utils/assetPath'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
|
||||
@@ -91,12 +92,16 @@ function LocationView({
|
||||
<h2 className="centered-heading">
|
||||
{getTranslatedText(location.name)}
|
||||
{location.danger_level !== undefined && location.danger_level === 0 && (
|
||||
<span className="danger-badge danger-safe" title="Safe Zone">✓ Safe</span>
|
||||
<GameTooltip content="Safe Zone">
|
||||
<span className="danger-badge danger-safe">✓ Safe</span>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{location.danger_level !== undefined && location.danger_level > 0 && (
|
||||
<span className={`danger-badge danger-${location.danger_level}`} title={`Danger Level: ${location.danger_level}`}>
|
||||
⚠️ {location.danger_level}
|
||||
</span>
|
||||
<GameTooltip content={`Danger Level: ${location.danger_level}`}>
|
||||
<span className={`danger-badge danger-${location.danger_level}`}>
|
||||
⚠️ {location.danger_level}
|
||||
</span>
|
||||
</GameTooltip>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
@@ -110,24 +115,24 @@ function LocationView({
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={`location-tag tag-${tag} ${isClickable ? 'clickable' : ''}`}
|
||||
title={isClickable ? `Click to ${tag === 'workbench' ? 'craft items' : 'repair items'}` : `This location has: ${tag}`}
|
||||
onClick={isClickable ? handleClick : undefined}
|
||||
style={isClickable ? { cursor: 'pointer' } : undefined}
|
||||
>
|
||||
{tag === 'workbench' && t('tags.workbench')}
|
||||
{tag === 'repair_station' && t('tags.repairStation')}
|
||||
{tag === 'safe_zone' && t('tags.safeZone')}
|
||||
{tag === 'shop' && t('tags.shop')}
|
||||
{tag === 'shelter' && t('tags.shelter')}
|
||||
{tag === 'medical' && t('tags.medical')}
|
||||
{tag === 'storage' && t('tags.storage')}
|
||||
{tag === 'water_source' && t('tags.water')}
|
||||
{tag === 'food_source' && t('tags.food')}
|
||||
{tag !== 'workbench' && tag !== 'repair_station' && tag !== 'safe_zone' && tag !== 'shop' && tag !== 'shelter' && tag !== 'medical' && tag !== 'storage' && tag !== 'water_source' && tag !== 'food_source' && `🏷️ ${tag}`}
|
||||
</span>
|
||||
<GameTooltip key={i} content={isClickable ? `Click to ${tag === 'workbench' ? 'craft items' : 'repair items'}` : `This location has: ${tag}`}>
|
||||
<span
|
||||
className={`location-tag tag-${tag} ${isClickable ? 'clickable' : ''}`}
|
||||
onClick={isClickable ? handleClick : undefined}
|
||||
style={isClickable ? { cursor: 'pointer' } : undefined}
|
||||
>
|
||||
{tag === 'workbench' && t('tags.workbench')}
|
||||
{tag === 'repair_station' && t('tags.repairStation')}
|
||||
{tag === 'safe_zone' && t('tags.safeZone')}
|
||||
{tag === 'shop' && t('tags.shop')}
|
||||
{tag === 'shelter' && t('tags.shelter')}
|
||||
{tag === 'medical' && t('tags.medical')}
|
||||
{tag === 'storage' && t('tags.storage')}
|
||||
{tag === 'water_source' && t('tags.water')}
|
||||
{tag === 'food_source' && t('tags.food')}
|
||||
{tag !== 'workbench' && tag !== 'repair_station' && tag !== 'safe_zone' && tag !== 'shop' && tag !== 'shelter' && tag !== 'medical' && tag !== 'storage' && tag !== 'water_source' && tag !== 'food_source' && `🏷️ ${tag}`}
|
||||
</span>
|
||||
</GameTooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -257,14 +262,15 @@ function LocationView({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="corpse-item-loot-btn"
|
||||
onClick={() => onLootCorpseItem(String(corpse.id), item.index)}
|
||||
disabled={!item.can_loot}
|
||||
title={!item.can_loot ? `Requires ${getTranslatedText(item.required_tool_name)}` : 'Loot this item'}
|
||||
>
|
||||
{item.can_loot ? `📦 ${t('common.loot')}` : '🔒'}
|
||||
</button>
|
||||
<GameTooltip content={!item.can_loot ? `Requires ${getTranslatedText(item.required_tool_name)}` : 'Loot this item'}>
|
||||
<button
|
||||
className="corpse-item-loot-btn"
|
||||
onClick={() => onLootCorpseItem(String(corpse.id), item.index)}
|
||||
disabled={!item.can_loot}
|
||||
>
|
||||
{item.can_loot ? `📦 ${t('common.loot')}` : '🔒'}
|
||||
</button>
|
||||
</GameTooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -328,39 +334,42 @@ function LocationView({
|
||||
{item.quantity > 1 && <div className="entity-quantity">×{item.quantity}</div>}
|
||||
</div>
|
||||
<div className="item-info-btn-container">
|
||||
<button className="entity-action-btn info" title="Item Info">{t('common.info')}</button>
|
||||
<div className="item-info-tooltip">
|
||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||
{item.weight !== undefined && item.weight > 0 && (
|
||||
<div className="item-tooltip-stat">
|
||||
⚖️ {t('stats.weight')}: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
|
||||
</div>
|
||||
)}
|
||||
{item.volume !== undefined && item.volume > 0 && (
|
||||
<div className="item-tooltip-stat">
|
||||
📦 {t('stats.volume')}: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
|
||||
</div>
|
||||
)}
|
||||
{item.hp_restore && item.hp_restore > 0 && (
|
||||
<div className="item-tooltip-stat">❤️ {t('stats.hpRestore')}: +{item.hp_restore}</div>
|
||||
)}
|
||||
{item.stamina_restore && item.stamina_restore > 0 && (
|
||||
<div className="item-tooltip-stat">⚡ {t('stats.staminaRestore')}: +{item.stamina_restore}</div>
|
||||
)}
|
||||
{item.damage_min !== undefined && item.damage_max !== undefined && (item.damage_min > 0 || item.damage_max > 0) && (
|
||||
<div className="item-tooltip-stat">
|
||||
⚔️ {t('stats.damage')}: {item.damage_min}-{item.damage_max}
|
||||
</div>
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && item.max_durability !== undefined && item.max_durability !== null && (
|
||||
<div className="item-tooltip-stat">
|
||||
🔧 {t('stats.durability')}: {item.durability}/{item.max_durability}
|
||||
</div>
|
||||
)}
|
||||
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
||||
<div className="item-tooltip-stat">⭐ {t('stats.tier')}: {item.tier}</div>
|
||||
)}
|
||||
</div>
|
||||
<GameTooltip content={
|
||||
<div className="item-info-tooltip-content">
|
||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||
{item.weight !== undefined && item.weight > 0 && (
|
||||
<div className="item-tooltip-stat">
|
||||
⚖️ {t('stats.weight')}: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
|
||||
</div>
|
||||
)}
|
||||
{item.volume !== undefined && item.volume > 0 && (
|
||||
<div className="item-tooltip-stat">
|
||||
📦 {t('stats.volume')}: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
|
||||
</div>
|
||||
)}
|
||||
{item.hp_restore && item.hp_restore > 0 && (
|
||||
<div className="item-tooltip-stat">❤️ {t('stats.hpRestore')}: +{item.hp_restore}</div>
|
||||
)}
|
||||
{item.stamina_restore && item.stamina_restore > 0 && (
|
||||
<div className="item-tooltip-stat">⚡ {t('stats.staminaRestore')}: +{item.stamina_restore}</div>
|
||||
)}
|
||||
{item.damage_min !== undefined && item.damage_max !== undefined && (item.damage_min > 0 || item.damage_max > 0) && (
|
||||
<div className="item-tooltip-stat">
|
||||
⚔️ {t('stats.damage')}: {item.damage_min}-{item.damage_max}
|
||||
</div>
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && item.max_durability !== undefined && item.max_durability !== null && (
|
||||
<div className="item-tooltip-stat">
|
||||
🔧 {t('stats.durability')}: {item.durability}/{item.max_durability}
|
||||
</div>
|
||||
)}
|
||||
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
||||
<div className="item-tooltip-stat">⭐ {t('stats.tier')}: {item.tier}</div>
|
||||
)}
|
||||
</div>
|
||||
}>
|
||||
<button className="entity-action-btn info">{t('common.info')}</button>
|
||||
</GameTooltip>
|
||||
</div>
|
||||
{item.quantity === 1 ? (
|
||||
<button
|
||||
@@ -425,13 +434,14 @@ function LocationView({
|
||||
)}
|
||||
</div>
|
||||
{player.can_pvp && (
|
||||
<button
|
||||
className="pvp-btn"
|
||||
onClick={() => onInitiatePvP(player.id)}
|
||||
title={`Attack ${player.name || player.username}`}
|
||||
>
|
||||
{t('game.attack')}
|
||||
</button>
|
||||
<GameTooltip content={`Attack ${player.name || player.username}`}>
|
||||
<button
|
||||
className="pvp-btn"
|
||||
onClick={() => onInitiatePvP(player.id)}
|
||||
>
|
||||
{t('game.attack')}
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{!player.can_pvp && player.level_diff !== undefined && Math.abs(player.level_diff) > 3 && (
|
||||
<div className="pvp-disabled-reason">{t('game.levelDifferenceTooHigh')}</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getAssetPath } from '../../utils/assetPath'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
import { GameTooltip } from '../common/GameTooltip'
|
||||
|
||||
interface MovementControlsProps {
|
||||
location: Location
|
||||
@@ -77,24 +78,29 @@ function MovementControls({
|
||||
movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) :
|
||||
combatState ? t('messages.cannotTravelCombat') :
|
||||
insufficientStamina ? t('messages.notEnoughStamina', { need: stamina, have: profile?.stamina ?? 0 }) :
|
||||
available ? `${destination}\n${t('game.distance')}: ${distance}m\n${t('game.stamina')}: ${stamina}` :
|
||||
t('messages.cannotGo', { direction: t('directions.' + direction) })
|
||||
available ? (
|
||||
<div className="movement-tooltip">
|
||||
<div className="tooltip-title">{destination}</div>
|
||||
<div className="tooltip-stat">📏 {t('game.distance')}: {distance}m</div>
|
||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {stamina}</div>
|
||||
</div>
|
||||
) : t('messages.cannotGo', { direction: t('directions.' + direction) })
|
||||
|
||||
return (
|
||||
<button
|
||||
key={direction}
|
||||
className={`compass-btn ${className} ${disabled ? 'disabled' : ''} ${combatState ? 'in-combat' : ''}`}
|
||||
onClick={() => onMove(direction)}
|
||||
disabled={disabled}
|
||||
title={tooltipText}
|
||||
>
|
||||
<span className="compass-arrow">{arrow}</span>
|
||||
{available && movementCooldown > 0 ? (
|
||||
<span className="compass-cost" title={t('messages.waitBeforeMoving', { seconds: movementCooldown })}>⏳{movementCooldown}s</span>
|
||||
) : available && (
|
||||
<span className="compass-cost">⚡{stamina}</span>
|
||||
)}
|
||||
</button>
|
||||
<GameTooltip key={direction} content={tooltipText}>
|
||||
<button
|
||||
className={`compass-btn ${className} ${disabled ? 'disabled' : ''} ${combatState ? 'in-combat' : ''}`}
|
||||
onClick={() => onMove(direction)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="compass-arrow">{arrow}</span>
|
||||
{available && movementCooldown > 0 ? (
|
||||
<span className="compass-cost">⏳{movementCooldown}s</span>
|
||||
) : available && (
|
||||
<span className="compass-cost">⚡{stamina}</span>
|
||||
)}
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,64 +137,95 @@ function MovementControls({
|
||||
{/* Special movements */}
|
||||
<div className="special-moves">
|
||||
{location.directions.includes('up') && (
|
||||
<button
|
||||
onClick={() => onMove('up')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.up')}\n${t('game.stamina')}: ${getStaminaCost('up')}`}
|
||||
>
|
||||
⬆️ {t('directions.up')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('up')}`}</span>
|
||||
</button>
|
||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||
<div className="movement-tooltip">
|
||||
<div className="tooltip-title">{t('directions.up')}</div>
|
||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('up')}</div>
|
||||
</div>
|
||||
)}>
|
||||
<button
|
||||
onClick={() => onMove('up')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
>
|
||||
⬆️ {t('directions.up')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('up')}`}</span>
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{location.directions.includes('down') && (
|
||||
<button
|
||||
onClick={() => onMove('down')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.down')}\n${t('game.stamina')}: ${getStaminaCost('down')}`}
|
||||
>
|
||||
⬇️ {t('directions.down')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('down')}`}</span>
|
||||
</button>
|
||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||
<div className="movement-tooltip">
|
||||
<div className="tooltip-title">{t('directions.down')}</div>
|
||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('down')}</div>
|
||||
</div>
|
||||
)}>
|
||||
<button
|
||||
onClick={() => onMove('down')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
>
|
||||
⬇️ {t('directions.down')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('down')}`}</span>
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{location.directions.includes('enter') && (
|
||||
<button
|
||||
onClick={() => onMove('enter')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.enter')}\n${t('game.stamina')}: ${getStaminaCost('enter')}`}
|
||||
>
|
||||
🚪 {t('directions.enter')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('enter')}`}</span>
|
||||
</button>
|
||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||
<div className="movement-tooltip">
|
||||
<div className="tooltip-title">{t('directions.enter')}</div>
|
||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('enter')}</div>
|
||||
</div>
|
||||
)}>
|
||||
<button
|
||||
onClick={() => onMove('enter')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
>
|
||||
🚪 {t('directions.enter')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('enter')}`}</span>
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{location.directions.includes('inside') && (
|
||||
<button
|
||||
onClick={() => onMove('inside')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.inside')}\n${t('game.stamina')}: ${getStaminaCost('inside')}`}
|
||||
>
|
||||
🚪 {t('directions.inside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('inside')}`}</span>
|
||||
</button>
|
||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||
<div className="movement-tooltip">
|
||||
<div className="tooltip-title">{t('directions.inside')}</div>
|
||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('inside')}</div>
|
||||
</div>
|
||||
)}>
|
||||
<button
|
||||
onClick={() => onMove('inside')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
>
|
||||
🚪 {t('directions.inside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('inside')}`}</span>
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{location.directions.includes('exit') && (
|
||||
<button
|
||||
onClick={() => onMove('exit')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : t('directions.exit')}
|
||||
>
|
||||
🚪 {t('directions.exit')}
|
||||
</button>
|
||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : t('directions.exit')}>
|
||||
<button
|
||||
onClick={() => onMove('exit')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
>
|
||||
🚪 {t('directions.exit')}
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
{location.directions.includes('outside') && (
|
||||
<button
|
||||
onClick={() => onMove('outside')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.outside')}\n${t('game.stamina')}: ${getStaminaCost('outside')}`}
|
||||
>
|
||||
🚪 {t('directions.outside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('outside')}`}</span>
|
||||
</button>
|
||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||
<div className="movement-tooltip">
|
||||
<div className="tooltip-title">{t('directions.outside')}</div>
|
||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('outside')}</div>
|
||||
</div>
|
||||
)}>
|
||||
<button
|
||||
onClick={() => onMove('outside')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
>
|
||||
🚪 {t('directions.outside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('outside')}`}</span>
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,28 +265,28 @@ function MovementControls({
|
||||
const insufficientStamina = profile ? profile.stamina < staminaCost : false
|
||||
|
||||
return (
|
||||
<button
|
||||
key={action.id}
|
||||
className={`interact-btn ${insufficientStamina ? 'disabled' : ''}`}
|
||||
disabled={!!combatState || cooldownRemaining > 0 || insufficientStamina || (profile?.is_dead ?? false)}
|
||||
onClick={() => onInteract && onInteract(interactable.instance_id, action.id)}
|
||||
title={
|
||||
profile?.is_dead
|
||||
? t('messages.youAreDead')
|
||||
: combatState
|
||||
? t('messages.cannotInteractInCombat')
|
||||
: insufficientStamina
|
||||
? t('messages.notEnoughStamina', { need: staminaCost, have: profile?.stamina ?? 0 })
|
||||
: cooldownRemaining > 0
|
||||
? t('messages.interactionCooldown', { seconds: cooldownRemaining })
|
||||
: getTranslatedText(action.description)
|
||||
}
|
||||
>
|
||||
{getTranslatedText(action.name)}
|
||||
<span className="stamina-cost">
|
||||
{cooldownRemaining > 0 ? `⏳${cooldownRemaining}s` : `⚡${staminaCost}`}
|
||||
</span>
|
||||
</button>
|
||||
<GameTooltip key={action.id} content={
|
||||
profile?.is_dead
|
||||
? t('messages.youAreDead')
|
||||
: combatState
|
||||
? t('messages.cannotInteractInCombat')
|
||||
: insufficientStamina
|
||||
? t('messages.notEnoughStamina', { need: staminaCost, have: profile?.stamina ?? 0 })
|
||||
: cooldownRemaining > 0
|
||||
? t('messages.interactionCooldown', { seconds: cooldownRemaining })
|
||||
: getTranslatedText(action.description)
|
||||
}>
|
||||
<button
|
||||
className={`interact-btn ${insufficientStamina ? 'disabled' : ''}`}
|
||||
disabled={!!combatState || cooldownRemaining > 0 || insufficientStamina || (profile?.is_dead ?? false)}
|
||||
onClick={() => onInteract && onInteract(interactable.instance_id, action.id)}
|
||||
>
|
||||
{getTranslatedText(action.name)}
|
||||
<span className="stamina-cost">
|
||||
{cooldownRemaining > 0 ? `⏳${cooldownRemaining}s` : `⚡${staminaCost}`}
|
||||
</span>
|
||||
</button>
|
||||
</GameTooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getAssetPath } from '../../utils/assetPath'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
import InventoryModal from './InventoryModal'
|
||||
import { GameProgressBar } from '../common/GameProgressBar'
|
||||
import { GameTooltip } from '../common/GameTooltip'
|
||||
|
||||
interface PlayerSidebarProps {
|
||||
playerState: PlayerState
|
||||
@@ -40,106 +41,118 @@ function PlayerSidebar({
|
||||
const [showInventory, setShowInventory] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const renderEquipmentSlot = (slot: string, item: any, label: string) => (
|
||||
<div className={`equipment-slot ${item ? 'filled' : 'empty'}`} title={!item ? label : ''}>
|
||||
{item ? (
|
||||
<>
|
||||
<button className="equipment-unequip-btn" onClick={() => onUnequipItem(slot)} title={t('game.unequip')}>✕</button>
|
||||
<div className="equipment-item-content">
|
||||
{item.image_path ? (
|
||||
<img
|
||||
src={getAssetPath(item.image_path)}
|
||||
alt={getTranslatedText(item.name)}
|
||||
className="equipment-emoji"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<span className="equipment-emoji" style={{ fontSize: '2rem' }}>{item.emoji}</span>
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && (
|
||||
<div className="equipment-durability-bar-container" style={{ width: '90%', marginTop: 'auto', marginBottom: '4px' }}>
|
||||
<GameProgressBar
|
||||
value={item.durability}
|
||||
max={item.max_durability}
|
||||
type="durability"
|
||||
height="4px"
|
||||
showText={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
const renderEquipmentSlot = (slot: string, item: any, label: string) => {
|
||||
// Construct the tooltip content if item exists
|
||||
const tooltipContent = item ? (
|
||||
<div className="game-tooltip-stats">
|
||||
<div className="item-tooltip-name" style={{ color: 'var(--game-text-highlight)', fontWeight: 'bold' }}>
|
||||
{getTranslatedText(item.name)}
|
||||
</div>
|
||||
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
||||
<div className="item-tooltip-stat" style={{ color: '#fbbf24' }}>
|
||||
⭐ Tier: {item.tier}
|
||||
</div>
|
||||
<div className="equipment-tooltip">
|
||||
<div className="item-tooltip-name">{getTranslatedText(item.name)}</div>
|
||||
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
||||
<div className="item-tooltip-stat" style={{ color: '#fbbf24' }}>
|
||||
⭐ Tier: {item.tier}
|
||||
</div>
|
||||
)}
|
||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||
{(item.unique_stats || item.stats) && Object.keys(item.unique_stats || item.stats).length > 0 && (
|
||||
<>
|
||||
{(item.unique_stats?.armor || item.stats?.armor) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.armor')}: +{item.unique_stats?.armor || item.stats?.armor}
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.hp_max || item.stats?.hp_max) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.hp')}: +{item.unique_stats?.hp_max || item.stats?.hp_max}
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.stamina_max || item.stats?.stamina_max) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.stamina')}: +{item.unique_stats?.stamina_max || item.stats?.stamina_max}
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.damage_min !== undefined || item.stats?.damage_min !== undefined) &&
|
||||
(item.unique_stats?.damage_max !== undefined || item.stats?.damage_max !== undefined) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.damage')}: {item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max}
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.weight')}: +{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.volume')}: +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && (
|
||||
)}
|
||||
{item.description && <div className="item-tooltip-desc" style={{ color: 'var(--game-text-secondary)', fontStyle: 'italic', marginBottom: '0.5rem' }}>{getTranslatedText(item.description)}</div>}
|
||||
{(item.unique_stats || item.stats) && Object.keys(item.unique_stats || item.stats).length > 0 && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'auto auto', gap: '0.25rem 1rem' }}>
|
||||
{(item.unique_stats?.armor || item.stats?.armor) && (
|
||||
<div className="item-tooltip-stat">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
|
||||
<span>{t('stats.durability')}:</span>
|
||||
<span>{item.durability}/{item.max_durability}</span>
|
||||
{t('stats.armor')}: <span style={{ color: 'var(--game-color-success)' }}>+{item.unique_stats?.armor || item.stats?.armor}</span>
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.hp_max || item.stats?.hp_max) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.hp')}: <span style={{ color: 'var(--game-color-success)' }}>+{item.unique_stats?.hp_max || item.stats?.hp_max}</span>
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.stamina_max || item.stats?.stamina_max) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.stamina')}: <span style={{ color: 'var(--game-color-stamina)' }}>+{item.unique_stats?.stamina_max || item.stats?.stamina_max}</span>
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.damage_min !== undefined || item.stats?.damage_min !== undefined) &&
|
||||
(item.unique_stats?.damage_max !== undefined || item.stats?.damage_max !== undefined) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.damage')}: <span style={{ color: 'var(--game-color-primary)' }}>{item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max}</span>
|
||||
</div>
|
||||
<GameProgressBar
|
||||
value={item.durability}
|
||||
max={item.max_durability}
|
||||
type="durability"
|
||||
height="6px"
|
||||
showText={false}
|
||||
/>
|
||||
)}
|
||||
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.weight')}: +{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg
|
||||
</div>
|
||||
)}
|
||||
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
|
||||
<div className="item-tooltip-stat">
|
||||
{t('stats.volume')}: +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={getAssetPath(`/images/placeholder/${slot}_placeholder.webp`)}
|
||||
alt={label}
|
||||
className="equipment-placeholder-img"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', opacity: 0.5 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && (
|
||||
<div className="item-tooltip-stat" style={{ marginTop: '0.5rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
|
||||
<span>{t('stats.durability')}:</span>
|
||||
<span>{item.durability}/{item.max_durability}</span>
|
||||
</div>
|
||||
<GameProgressBar
|
||||
value={item.durability}
|
||||
max={item.max_durability}
|
||||
type="durability"
|
||||
height="6px"
|
||||
showText={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : label; // Show label if no item
|
||||
|
||||
return (
|
||||
<GameTooltip content={tooltipContent}>
|
||||
<div className={`equipment-slot game-slot ${item ? 'filled' : 'empty'}`}>
|
||||
{item ? (
|
||||
<>
|
||||
<GameTooltip content={t('game.unequip')}>
|
||||
<button className="equipment-unequip-btn game-btn game-btn-icon" onClick={(e) => { e.stopPropagation(); onUnequipItem(slot); }}>✕</button>
|
||||
</GameTooltip>
|
||||
<div className="equipment-item-content">
|
||||
{item.image_path ? (
|
||||
<img
|
||||
src={getAssetPath(item.image_path)}
|
||||
alt={getTranslatedText(item.name)}
|
||||
className="equipment-emoji"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<span className="equipment-emoji" style={{ fontSize: '2rem' }}>{item.emoji}</span>
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && (
|
||||
<div className="equipment-durability-bar-container" style={{ width: '90%', marginTop: 'auto', marginBottom: '4px' }}>
|
||||
<GameProgressBar
|
||||
value={item.durability}
|
||||
max={item.max_durability}
|
||||
type="durability"
|
||||
height="4px"
|
||||
showText={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={getAssetPath(`/images/placeholder/${slot}_placeholder.webp`)}
|
||||
alt={label}
|
||||
className="equipment-placeholder-img"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', opacity: 0.5 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GameTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`right-sidebar mobile-menu-panel ${mobileMenuOpen === 'right' ? 'open' : ''}`}>
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
width: 95vw;
|
||||
max-width: 1400px;
|
||||
height: 85vh;
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 12px;
|
||||
background: var(--game-bg-modal);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--game-shadow-modal);
|
||||
overflow: hidden;
|
||||
color: var(--game-text-primary);
|
||||
font-family: var(--game-font-main);
|
||||
}
|
||||
|
||||
.workbench-header {
|
||||
@@ -31,14 +33,14 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid #4a5568;
|
||||
background: var(--game-bg-panel);
|
||||
border-bottom: 1px solid var(--game-border-color);
|
||||
}
|
||||
|
||||
.workbench-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #e2e8f0;
|
||||
color: var(--game-text-highlight);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -98,8 +100,8 @@
|
||||
|
||||
/* Column 1: Sidebar */
|
||||
.workbench-sidebar {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-right: 1px solid #3a4b5c;
|
||||
background: var(--game-bg-panel);
|
||||
border-right: 1px solid var(--game-border-color);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -142,9 +144,9 @@
|
||||
}
|
||||
|
||||
.workbench-sidebar .category-btn.active {
|
||||
background: rgba(66, 153, 225, 0.15);
|
||||
border-color: #4299e1;
|
||||
color: #63b3ed;
|
||||
background: rgba(234, 113, 66, 0.15);
|
||||
border-color: var(--game-color-primary);
|
||||
color: var(--game-color-primary);
|
||||
}
|
||||
|
||||
.workbench-sidebar .cat-icon {
|
||||
@@ -187,9 +189,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
background: var(--game-bg-card);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
gap: 0.5rem;
|
||||
@@ -201,20 +203,21 @@
|
||||
}
|
||||
|
||||
.workbench-item-card.selected {
|
||||
background: rgba(66, 153, 225, 0.1);
|
||||
border-color: #4299e1;
|
||||
background: rgba(234, 113, 66, 0.1);
|
||||
border-color: var(--game-color-primary);
|
||||
box-shadow: 0 0 0 1px var(--game-color-primary);
|
||||
}
|
||||
|
||||
.workbench-item-card.craftable {
|
||||
border-left: 3px solid #4caf50;
|
||||
border-left: 3px solid var(--game-color-success);
|
||||
}
|
||||
|
||||
.workbench-item-card.repairable {
|
||||
border-left: 3px solid #ff9800;
|
||||
border-left: 3px solid var(--game-color-warning);
|
||||
}
|
||||
|
||||
.workbench-item-card.salvageable {
|
||||
border-left: 3px solid #9c27b0;
|
||||
border-left: 3px solid var(--game-color-danger);
|
||||
}
|
||||
|
||||
.item-card-content {
|
||||
@@ -446,7 +449,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: var(--game-bg-panel);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
@@ -460,11 +463,11 @@
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 1.5rem auto;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--game-radius-md);
|
||||
overflow: hidden;
|
||||
border: 2px solid #4a5568;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid var(--game-border-color);
|
||||
background: var(--game-bg-input);
|
||||
box-shadow: var(--game-shadow-card);
|
||||
}
|
||||
|
||||
.detail-image {
|
||||
|
||||
@@ -1,11 +1,64 @@
|
||||
:root {
|
||||
font-family: 'Saira Condensed', system-ui, sans-serif;
|
||||
/* --- Core Colors (Mature/Industrial) --- */
|
||||
--game-bg-app: #050505;
|
||||
/* Deepest black */
|
||||
--game-bg-panel: rgba(18, 18, 24, 0.98);
|
||||
/* Almost solid panels */
|
||||
--game-bg-glass: rgba(10, 10, 15, 0.9);
|
||||
/* Overlays */
|
||||
--game-bg-slot: rgba(0, 0, 0, 0.6);
|
||||
/* Item slots */
|
||||
--game-bg-slot-hover: rgba(255, 255, 255, 0.1);
|
||||
--game-bg-tooltip: rgba(15, 15, 20, 0.98);
|
||||
|
||||
/* --- Borders & Separators --- */
|
||||
--game-border-color: rgba(255, 255, 255, 0.12);
|
||||
--game-border-active: rgba(255, 255, 255, 0.4);
|
||||
--game-border-highlight: #ff6b6b;
|
||||
/* Red accent border */
|
||||
|
||||
/* --- Dimensions --- */
|
||||
--game-radius-xs: 2px;
|
||||
--game-radius-sm: 4px;
|
||||
--game-radius-md: 6px;
|
||||
|
||||
/* --- Typography --- */
|
||||
--game-font-main: 'Saira Condensed', system-ui, sans-serif;
|
||||
--game-text-primary: #e0e0e0;
|
||||
--game-text-secondary: #94a3b8;
|
||||
--game-text-highlight: #fbbf24;
|
||||
--game-text-danger: #ef4444;
|
||||
|
||||
/* --- Semantic Colors --- */
|
||||
--game-color-primary: #e11d48;
|
||||
/* Blood Red */
|
||||
--game-color-stamina: #d97706;
|
||||
/* Amber */
|
||||
--game-color-magic: #3b82f6;
|
||||
/* Blue */
|
||||
--game-color-success: #10b981;
|
||||
/* Emerald */
|
||||
--game-color-warning: #f59e0b;
|
||||
/* Amber */
|
||||
|
||||
/* --- Rarity --- */
|
||||
--rarity-common: #9ca3af;
|
||||
--rarity-uncommon: #ffffff;
|
||||
--rarity-rare: #34d399;
|
||||
--rarity-epic: #60a5fa;
|
||||
--rarity-legendary: #fbbf24;
|
||||
|
||||
/* --- Effects --- */
|
||||
--game-shadow-panel: 0 8px 32px rgba(0, 0, 0, 0.8);
|
||||
--game-shadow-tooltip: 0 4px 12px rgba(0, 0, 0, 0.8);
|
||||
--game-shadow-glow: 0 0 15px rgba(225, 29, 72, 0.3);
|
||||
|
||||
font-family: var(--game-font-main);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #1a1a1a;
|
||||
color: var(--game-text-primary);
|
||||
background-color: var(--game-bg-app);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@@ -13,58 +66,105 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
/* --- Reusable Game Classes --- */
|
||||
|
||||
/* Panels */
|
||||
.game-panel {
|
||||
background: var(--game-bg-panel);
|
||||
border: 1px solid var(--game-border-color);
|
||||
box-shadow: var(--game-shadow-panel);
|
||||
border-radius: var(--game-radius-sm);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
.game-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: #1a1a1a;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
/* Buttons */
|
||||
.game-btn {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--game-border-color);
|
||||
color: var(--game-text-primary);
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #2a2a2a;
|
||||
font-family: var(--game-font-main);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: var(--game-radius-xs);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
.game-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--game-text-secondary);
|
||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
.game-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.game-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-btn-primary {
|
||||
background: rgba(225, 29, 72, 0.2);
|
||||
border-color: rgba(225, 29, 72, 0.5);
|
||||
color: #ffcccc;
|
||||
}
|
||||
|
||||
.game-btn-primary:hover {
|
||||
background: rgba(225, 29, 72, 0.3);
|
||||
border-color: var(--game-color-primary);
|
||||
box-shadow: var(--game-shadow-glow);
|
||||
}
|
||||
|
||||
.game-btn-icon {
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
/* Or keep square for industrial look */
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Slots */
|
||||
.game-slot {
|
||||
background: var(--game-bg-slot);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-xs);
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.game-slot:hover {
|
||||
background: var(--game-bg-slot-hover);
|
||||
border-color: var(--game-border-active);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Twemoji styles */
|
||||
img.emoji {
|
||||
height: 1em;
|
||||
|
||||
Reference in New Issue
Block a user