This commit is contained in:
Joan
2026-02-05 16:09:34 +01:00
parent 1b7ffd614d
commit ccf9ba3e28
31 changed files with 3713 additions and 13002 deletions

View File

@@ -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}
>

View File

@@ -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) {

View File

@@ -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' : ''}`}

View File

@@ -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;
}
}
}

View 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}
</>
);
};

View File

@@ -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);
}

View File

@@ -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>
);
})()
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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' : ''}`}>

View File

@@ -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 {

View File

@@ -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;