feat: Implement Inventory Grid View and GameButton

This commit is contained in:
Joan
2026-02-07 22:00:14 +01:00
parent dcfc91b82b
commit eb75ee5b33
22 changed files with 1161 additions and 256 deletions

View File

@@ -10,6 +10,8 @@ hit.wav (When anyone takes damage)
victory.wav (Combat won) victory.wav (Combat won)
defeat.wav (Combat lost) defeat.wav (Combat lost)
flee.wav (Successfully ran away) flee.wav (Successfully ran away)
step.wav (Movement between locations)
Combat - Player Weapons Combat - Player Weapons
The system detects keywords in the weapon name to pick the sound. If no match is found, it plays the default. The system detects keywords in the weapon name to pick the sound. If no match is found, it plays the default.

Binary file not shown.

View File

@@ -115,27 +115,6 @@ html {
text-shadow: 0 0 10px rgba(234, 113, 66, 0.3); text-shadow: 0 0 10px rgba(234, 113, 66, 0.3);
} }
/* Player Count Badge */
.player-count-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background-color: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
font-size: 0.8rem;
color: #aaa;
margin-right: 12px;
transition: all 0.2s ease;
}
.player-count-badge:hover {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.2);
color: #ddd;
}
.status-dot { .status-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -457,7 +436,7 @@ html {
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
padding: 0.3rem 0.8rem; padding: 0.3rem 0.8rem;
border-radius: var(--game-radius-sm); clip-path: var(--game-clip-path-sm);
font-size: 0.9rem; font-size: 0.9rem;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
@@ -516,11 +495,11 @@ html {
padding: 0.35rem 0.75rem; padding: 0.35rem 0.75rem;
background: rgba(107, 185, 240, 0.15); background: rgba(107, 185, 240, 0.15);
border: 1px solid rgba(107, 185, 240, 0.4); border: 1px solid rgba(107, 185, 240, 0.4);
border-radius: 16px;
font-size: 0.85rem; font-size: 0.85rem;
color: #6bb9f0; color: #6bb9f0;
font-weight: 500; font-weight: 500;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
} }
.location-tag:hover { .location-tag:hover {
@@ -708,8 +687,8 @@ html {
margin: 1rem auto; margin: 1rem auto;
aspect-ratio: 10 / 7; aspect-ratio: 10 / 7;
overflow: hidden; overflow: hidden;
border-radius: 8px;
border: 2px solid rgba(255, 107, 107, 0.3); border: 2px solid rgba(255, 107, 107, 0.3);
clip-path: var(--game-clip-path);
} }
.location-image { .location-image {
@@ -721,19 +700,19 @@ html {
.message-box { .message-box {
background: rgba(255, 107, 107, 0.2); background: rgba(255, 107, 107, 0.2);
padding: 1rem; padding: 1rem;
border-radius: 8px;
border-left: 4px solid #ff6b6b; border-left: 4px solid #ff6b6b;
color: #fff; color: #fff;
clip-path: var(--game-clip-path);
} }
/* Location Messages Log */ /* Location Messages Log */
.location-messages-log { .location-messages-log {
background: rgba(0, 0, 0, 0.3); background: var(--game-bg-panel);
border-radius: 8px; border: 1px solid var(--game-border-color);
border: 2px solid rgba(255, 107, 107, 0.2);
padding: 0.8rem; padding: 0.8rem;
margin-top: 1rem; margin-top: 1rem;
max-width: 100%; max-width: 100%;
clip-path: var(--game-clip-path);
} }
.location-messages-log h4 { .location-messages-log h4 {
@@ -744,7 +723,8 @@ html {
} }
.messages-scroll { .messages-scroll {
max-height: 150px; height: 5.5rem;
/* Compact fixed height (~3 lines) */
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -756,7 +736,7 @@ html {
gap: 0.5rem; gap: 0.5rem;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 4px; clip-path: var(--game-clip-path-sm);
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -804,9 +784,9 @@ html {
border: 1px solid var(--game-border-color); 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%); background: linear-gradient(135deg, rgba(80, 80, 90, 0.3) 0%, rgba(40, 40, 50, 0.5) 100%);
color: var(--game-text-primary); color: var(--game-text-primary);
border-radius: var(--game-radius-sm);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
clip-path: var(--game-clip-path-sm);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -879,9 +859,9 @@ html {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
border-radius: 12px; border: 1px solid rgba(255, 107, 107, 0.5);
border: 2px solid rgba(255, 107, 107, 0.5);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5);
clip-path: var(--game-clip-path-sm);
} }
.compass-icon { .compass-icon {
@@ -915,9 +895,9 @@ html {
.compass-center-btn { .compass-center-btn {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%;
border: 2px solid var(--game-color-primary); border: 2px solid var(--game-color-primary);
background: radial-gradient(circle, rgba(225, 29, 72, 0.2) 0%, rgba(20, 20, 20, 0.8) 100%); background: radial-gradient(circle, rgba(225, 29, 72, 0.2) 0%, rgba(20, 20, 20, 0.8) 100%);
clip-path: var(--game-clip-path-sm);
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -985,10 +965,10 @@ html {
margin: 0.5rem 0; margin: 0.5rem 0;
background: rgba(255, 152, 0, 0.2); background: rgba(255, 152, 0, 0.2);
border: 2px solid rgba(255, 152, 0, 0.5); border: 2px solid rgba(255, 152, 0, 0.5);
border-radius: 8px;
color: #ff9800; color: #ff9800;
font-weight: bold; font-weight: bold;
font-size: 1.1rem; font-size: 1.1rem;
clip-path: var(--game-clip-path-sm);
} }
/* Special movement buttons */ /* Special movement buttons */
@@ -1005,9 +985,9 @@ html {
background: rgba(107, 147, 255, 0.3); background: rgba(107, 147, 255, 0.3);
color: #fff; color: #fff;
font-size: 0.95rem; font-size: 0.95rem;
border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
clip-path: var(--game-clip-path-sm);
font-weight: 600; font-weight: 600;
} }
@@ -1034,10 +1014,10 @@ html {
border: none; border: none;
background: rgba(76, 175, 80, 0.3); background: rgba(76, 175, 80, 0.3);
color: #fff; color: #fff;
border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
font-size: 0.95rem; font-size: 0.95rem;
clip-path: var(--game-clip-path-sm);
} }
.action-button:hover { .action-button:hover {
@@ -1047,10 +1027,10 @@ html {
/* Interactables Section */ /* Interactables Section */
.interactables-section { .interactables-section {
background: rgba(0, 0, 0, 0.3); background: var(--game-bg-panel);
padding: 1.5rem; padding: 1.5rem;
border-radius: 10px; border: 1px solid var(--game-border-color);
border: 2px solid rgba(255, 193, 7, 0.3); clip-path: var(--game-clip-path);
} }
.interactables-section h3 { .interactables-section h3 {
@@ -1065,13 +1045,13 @@ body.no-scroll {
/* Interactable Card with Image */ /* Interactable Card with Image */
.interactable-card { .interactable-card {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
margin-bottom: 1rem; margin-bottom: 1rem;
border: 2px solid rgba(255, 193, 7, 0.4); border: 1px solid rgba(255, 193, 7, 0.4);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: all 0.3s; transition: all 0.3s;
clip-path: var(--game-clip-path);
} }
.interactable-card:hover { .interactable-card:hover {
@@ -1136,10 +1116,10 @@ body.no-scroll {
border: none; border: none;
background: rgba(255, 193, 7, 0.3); background: rgba(255, 193, 7, 0.3);
color: #fff; color: #fff;
border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
font-size: 0.9rem; font-size: 0.9rem;
clip-path: var(--game-clip-path-sm);
} }
.interact-btn:hover { .interact-btn:hover {
@@ -1160,10 +1140,10 @@ body.no-scroll {
} }
.entity-section { .entity-section {
background: rgba(0, 0, 0, 0.4); background: var(--game-bg-panel);
padding: 1.5rem; padding: 1.5rem;
border-radius: 10px; border: 1px solid var(--game-border-color);
border: 2px solid rgba(255, 107, 107, 0.3); clip-path: var(--game-clip-path);
} }
.entity-section h3 { .entity-section h3 {
@@ -1210,13 +1190,13 @@ body.no-scroll {
.entity-card { .entity-card {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
padding: 1rem; padding: 1rem;
border-radius: 8px;
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
transition: all 0.3s; transition: all 0.3s;
min-width: 320px; min-width: 320px;
clip-path: var(--game-clip-path);
} }
.entity-card:hover { .entity-card:hover {
@@ -1306,12 +1286,12 @@ body.no-scroll {
border: none; border: none;
background: linear-gradient(135deg, #6bb9f0, #89d4ff); background: linear-gradient(135deg, #6bb9f0, #89d4ff);
color: white; color: white;
border-radius: 6px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
transition: all 0.3s; transition: all 0.3s;
white-space: nowrap; white-space: nowrap;
clip-path: var(--game-clip-path-sm);
} }
.entity-action-btn:hover { .entity-action-btn:hover {
@@ -1375,9 +1355,9 @@ body.no-scroll {
.corpse-details { .corpse-details {
background: rgba(156, 39, 176, 0.1); background: rgba(156, 39, 176, 0.1);
border: 2px solid rgba(156, 39, 176, 0.3); border: 2px solid rgba(156, 39, 176, 0.3);
border-radius: 8px;
padding: 1rem; padding: 1rem;
animation: fadeIn 0.2s ease-out; animation: fadeIn 0.2s ease-out;
clip-path: var(--game-clip-path);
} }
@keyframes fadeIn { @keyframes fadeIn {
@@ -1434,9 +1414,9 @@ body.no-scroll {
align-items: center; align-items: center;
padding: 0.75rem; padding: 0.75rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path);
} }
.corpse-item:hover { .corpse-item:hover {
@@ -1518,11 +1498,11 @@ body.no-scroll {
border: none; border: none;
background: linear-gradient(135deg, #9c27b0, #ba68c8); background: linear-gradient(135deg, #9c27b0, #ba68c8);
color: white; color: white;
border-radius: 6px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
font-size: 0.95rem; font-size: 0.95rem;
transition: all 0.3s; transition: all 0.3s;
clip-path: var(--game-clip-path);
} }
.loot-all-btn:hover { .loot-all-btn:hover {
@@ -1535,9 +1515,9 @@ body.no-scroll {
width: 100px; width: 100px;
height: 70px; height: 70px;
flex-shrink: 0; flex-shrink: 0;
border-radius: 4px;
overflow: hidden; overflow: hidden;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
clip-path: var(--game-clip-path-sm);
} }
.entity-image img { .entity-image img {
@@ -1563,9 +1543,9 @@ body.no-scroll {
.inventory-panel { .inventory-panel {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
padding: 1.5rem; padding: 1.5rem;
border-radius: 10px;
border: 2px solid rgba(107, 147, 255, 0.3); border: 2px solid rgba(107, 147, 255, 0.3);
height: fit-content; height: fit-content;
clip-path: var(--game-clip-path);
} }
.inventory-panel h3 { .inventory-panel h3 {
@@ -1702,8 +1682,8 @@ body.no-scroll {
.profile-section { .profile-section {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
padding: 1.5rem; padding: 1.5rem;
border-radius: 10px;
border: 1px solid rgba(255, 107, 107, 0.3); border: 1px solid rgba(255, 107, 107, 0.3);
clip-path: var(--game-clip-path);
} }
.profile-section h3 { .profile-section h3 {
@@ -1718,7 +1698,7 @@ body.no-scroll {
padding: 0.75rem; padding: 0.75rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
border-radius: 5px; clip-path: var(--game-clip-path-sm);
} }
.stat-row.highlight { .stat-row.highlight {
@@ -1732,11 +1712,11 @@ body.no-scroll {
border: none; border: none;
background: #ff6b6b; background: #ff6b6b;
color: white; color: white;
border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
clip-path: var(--game-clip-path-sm);
} }
.button-primary:hover { .button-primary:hover {
@@ -1747,10 +1727,10 @@ body.no-scroll {
/* Profile Sidebar */ /* Profile Sidebar */
.profile-sidebar { .profile-sidebar {
background: rgba(0, 0, 0, 0.3); background: var(--game-bg-panel);
border-radius: 10px;
padding: 1rem; padding: 1rem;
border: 2px solid rgba(255, 107, 107, 0.3); border: 1px solid var(--game-border-color);
clip-path: var(--game-clip-path);
} }
.profile-sidebar h3 { .profile-sidebar h3 {
@@ -1786,13 +1766,13 @@ body.no-scroll {
.sidebar-progress-bar { .sidebar-progress-bar {
height: 24px; height: 24px;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 6px;
overflow: visible; overflow: visible;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
clip-path: var(--game-clip-path-sm);
} }
.sidebar-progress-fill { .sidebar-progress-fill {
@@ -1801,7 +1781,6 @@ body.no-scroll {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
border-radius: 6px;
} }
.progress-percentage { .progress-percentage {
@@ -1842,8 +1821,8 @@ body.no-scroll {
margin: 1rem 0; margin: 1rem 0;
padding: 1rem; padding: 1rem;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
border: 1px solid rgba(255, 107, 107, 0.3); border: 1px solid rgba(255, 107, 107, 0.3);
clip-path: var(--game-clip-path-sm);
} }
.capacity-info { .capacity-info {
@@ -1874,16 +1853,15 @@ body.no-scroll {
.capacity-bar { .capacity-bar {
height: 20px; height: 20px;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(255, 107, 107, 0.2); border: 1px solid rgba(255, 107, 107, 0.2);
position: relative; position: relative;
clip-path: var(--game-clip-path-sm);
} }
.capacity-fill { .capacity-fill {
height: 100%; height: 100%;
transition: width 0.3s ease; transition: width 0.3s ease;
border-radius: 10px;
} }
/* Single color for capacity bars */ /* Single color for capacity bars */
@@ -1918,17 +1896,17 @@ body.no-scroll {
gap: 0.5rem; gap: 0.5rem;
padding: 0.4rem 0.5rem; padding: 0.4rem 0.5rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
font-size: 0.9rem; font-size: 0.9rem;
clip-path: var(--game-clip-path-sm);
} }
.stat-plus-btn { .stat-plus-btn {
background: rgba(107, 185, 240, 0.3); background: rgba(107, 185, 240, 0.3);
border: 1px solid #6bb9f0; border: 1px solid #6bb9f0;
color: #6bb9f0; color: #6bb9f0;
border-radius: 4px;
width: 24px; width: 24px;
height: 24px; height: 24px;
clip-path: var(--game-clip-path-sm);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1974,11 +1952,11 @@ body.no-scroll {
/* Equipment Sidebar */ /* Equipment Sidebar */
.equipment-sidebar { .equipment-sidebar {
background: rgba(0, 0, 0, 0.3); background: var(--game-bg-panel);
border-radius: 10px;
padding: 1rem; padding: 1rem;
border: 1px solid rgba(255, 107, 107, 0.3); border: 1px solid var(--game-border-color);
overflow: visible; overflow: visible;
clip-path: var(--game-clip-path);
/* Allow tooltips to overflow */ /* Allow tooltips to overflow */
} }
@@ -2029,12 +2007,12 @@ body.no-scroll {
.equipment-slot { .equipment-slot {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 0.5rem; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
clip-path: var(--game-clip-path);
/* Changed from center to space-between */ /* Changed from center to space-between */
gap: 0.25rem; gap: 0.25rem;
/* Fixed dimensions for consistent sizing */ /* Fixed dimensions for consistent sizing */
@@ -2110,9 +2088,9 @@ body.no-scroll {
right: 4px; right: 4px;
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: 50%;
background: rgba(244, 67, 54, 0.9); background: rgba(244, 67, 54, 0.9);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
clip-path: var(--game-clip-path-sm);
color: #fff; color: #fff;
font-size: 0.7rem; font-size: 0.7rem;
font-weight: bold; font-weight: bold;
@@ -2141,8 +2119,8 @@ body.no-scroll {
transform: translateX(-50%); transform: translateX(-50%);
background: rgba(30, 30, 30, 0.98); background: rgba(30, 30, 30, 0.98);
border: 2px solid #ff6b6b; border: 2px solid #ff6b6b;
border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
clip-path: var(--game-clip-path-sm);
min-width: 220px; min-width: 220px;
max-width: 300px; max-width: 300px;
z-index: 10000; z-index: 10000;
@@ -2196,9 +2174,9 @@ body.no-scroll {
font-size: 0.7rem; font-size: 0.7rem;
/* Slightly larger font */ /* Slightly larger font */
border: none; border: none;
border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
color: #fff; color: #fff;
display: flex; display: flex;
@@ -2293,7 +2271,7 @@ body.no-scroll {
color: #6bb9f0; color: #6bb9f0;
background: rgba(107, 185, 240, 0.1); background: rgba(107, 185, 240, 0.1);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; clip-path: var(--game-clip-path-sm);
} }
.equipment-slot-label { .equipment-slot-label {
@@ -2305,10 +2283,10 @@ body.no-scroll {
/* Inventory Sidebar */ /* Inventory Sidebar */
.inventory-sidebar { .inventory-sidebar {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 1rem; padding: 1rem;
padding-top: 3rem; padding-top: 3rem;
border: 1px solid rgba(107, 185, 240, 0.3); border: 1px solid rgba(107, 185, 240, 0.3);
clip-path: var(--game-clip-path);
overflow: visible; overflow: visible;
/* Allow tooltips and dropdowns to show */ /* Allow tooltips and dropdowns to show */
position: relative; position: relative;
@@ -2336,10 +2314,10 @@ body.no-scroll {
aspect-ratio: 1; aspect-ratio: 1;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
clip-path: var(--game-clip-path-sm);
transition: all 0.2s; transition: all 0.2s;
cursor: pointer; cursor: pointer;
} }
@@ -2387,7 +2365,7 @@ body.no-scroll {
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 600; font-weight: 600;
padding: 1px 4px; padding: 1px 4px;
border-radius: 3px; clip-path: var(--game-clip-path-sm);
} }
.sidebar-empty { .sidebar-empty {
@@ -2465,9 +2443,9 @@ body.no-scroll {
.inventory-item-row-hover { .inventory-item-row-hover {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 0.75rem; padding: 0.75rem;
display: flex; display: flex;
clip-path: var(--game-clip-path-sm);
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -2501,9 +2479,9 @@ body.no-scroll {
left: 0; left: 0;
background: rgba(30, 30, 30, 0.98); background: rgba(30, 30, 30, 0.98);
border: 2px solid #6bb9f0; border: 2px solid #6bb9f0;
border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
min-width: 220px; min-width: 220px;
clip-path: var(--game-clip-path-sm);
max-width: 300px; max-width: 300px;
z-index: 10000; z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
@@ -2540,10 +2518,10 @@ body.no-scroll {
border: 1px solid #6bb9f0; border: 1px solid #6bb9f0;
color: #6bb9f0; color: #6bb9f0;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
white-space: nowrap; white-space: nowrap;
min-width: 60px; min-width: 60px;
} }
@@ -2606,9 +2584,9 @@ body.no-scroll {
right: 0; right: 0;
background: rgba(30, 30, 30, 0.98); background: rgba(30, 30, 30, 0.98);
border: 2px solid #6bb9f0; border: 2px solid #6bb9f0;
border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
min-width: 220px; min-width: 220px;
clip-path: var(--game-clip-path-sm);
max-width: 300px; max-width: 300px;
z-index: 10000; z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
@@ -2710,9 +2688,9 @@ body.no-scroll {
padding-bottom: 4px; padding-bottom: 4px;
background: rgba(30, 30, 30, 0.98); background: rgba(30, 30, 30, 0.98);
border: 2px solid #f44336; border: 2px solid #f44336;
border-radius: 8px;
padding: 0.5rem; padding: 0.5rem;
min-width: 120px; min-width: 120px;
clip-path: var(--game-clip-path-sm);
z-index: 10000; z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
flex-direction: column; flex-direction: column;
@@ -2741,10 +2719,10 @@ body.no-scroll {
border: 1px solid #f44336; border: 1px solid #f44336;
color: #f44336; color: #f44336;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
} }
@@ -2769,9 +2747,9 @@ body.no-scroll {
padding-bottom: 4px; padding-bottom: 4px;
background: rgba(30, 30, 30, 0.98); background: rgba(30, 30, 30, 0.98);
border: 2px solid #4caf50; border: 2px solid #4caf50;
border-radius: 8px;
padding: 0.5rem; padding: 0.5rem;
min-width: 140px; min-width: 140px;
clip-path: var(--game-clip-path-sm);
z-index: 10000; z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
flex-direction: column; flex-direction: column;
@@ -2799,10 +2777,10 @@ body.no-scroll {
border: 1px solid #4caf50; border: 1px solid #4caf50;
color: #4caf50; color: #4caf50;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
} }
@@ -2823,10 +2801,10 @@ body.no-scroll {
.inventory-item-row { .inventory-item-row {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 0.5rem; padding: 0.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
clip-path: var(--game-clip-path-sm);
gap: 0.75rem; gap: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -2854,8 +2832,8 @@ body.no-scroll {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
clip-path: var(--game-clip-path-sm);
} }
.item-icon-small img { .item-icon-small img {
@@ -2888,7 +2866,7 @@ body.no-scroll {
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 600; font-weight: 600;
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; clip-path: var(--game-clip-path-sm);
} }
/* Category Filter */ /* Category Filter */
@@ -2903,10 +2881,10 @@ body.no-scroll {
padding: 0.5rem; padding: 0.5rem;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -2956,9 +2934,9 @@ body.no-scroll {
.item-actions-panel { .item-actions-panel {
background: rgba(107, 185, 240, 0.1); background: rgba(107, 185, 240, 0.1);
border: 2px solid #6bb9f0; border: 2px solid #6bb9f0;
border-radius: 8px;
padding: 1rem; padding: 1rem;
margin-top: 1rem; margin-top: 1rem;
clip-path: var(--game-clip-path-sm);
} }
.item-details-header { .item-details-header {
@@ -3010,11 +2988,11 @@ body.no-scroll {
.item-action-btn { .item-action-btn {
padding: 0.6rem; padding: 0.6rem;
border: none; border: none;
border-radius: 6px;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
} }
.item-action-btn.use-btn { .item-action-btn.use-btn {
@@ -3110,9 +3088,9 @@ body.no-scroll {
width: 80px; width: 80px;
height: 56px; height: 56px;
overflow: hidden; overflow: hidden;
border-radius: 8px;
margin-right: 1rem; margin-right: 1rem;
flex-shrink: 0; flex-shrink: 0;
clip-path: var(--game-clip-path-sm);
} }
.entity-image img { .entity-image img {
@@ -3140,12 +3118,12 @@ body.no-scroll {
.combat-modal { .combat-modal {
background: linear-gradient(135deg, #1a1a2e, #16213e); background: linear-gradient(135deg, #1a1a2e, #16213e);
border: 2px solid rgba(107, 185, 240, 0.3); border: 2px solid rgba(107, 185, 240, 0.3);
border-radius: 16px;
padding: 2rem; padding: 2rem;
max-width: 600px; max-width: 600px;
width: 90%; width: 90%;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s; animation: slideUp 0.3s;
clip-path: var(--game-clip-path);
} }
.combat-header { .combat-header {
@@ -3169,16 +3147,16 @@ body.no-scroll {
padding: 1.5rem; padding: 1.5rem;
background: rgba(244, 67, 54, 0.1); background: rgba(244, 67, 54, 0.1);
border: 2px solid rgba(244, 67, 54, 0.3); border: 2px solid rgba(244, 67, 54, 0.3);
border-radius: 12px; clip-path: var(--game-clip-path-sm);
} }
.combat-enemy-image-container { .combat-enemy-image-container {
width: 200px; width: 200px;
height: 140px; height: 140px;
flex-shrink: 0; flex-shrink: 0;
border-radius: 8px;
overflow: hidden; overflow: hidden;
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
clip-path: var(--game-clip-path-sm);
} }
.combat-enemy-image { .combat-enemy-image {
@@ -3211,9 +3189,9 @@ body.no-scroll {
width: 100%; width: 100%;
height: 24px; height: 24px;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 12px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(244, 67, 54, 0.4); border: 1px solid rgba(244, 67, 54, 0.4);
clip-path: var(--game-clip-path-sm);
} }
.combat-hp-fill { .combat-hp-fill {
@@ -3228,10 +3206,10 @@ body.no-scroll {
padding: 1rem; padding: 1rem;
background: rgba(107, 185, 240, 0.1); background: rgba(107, 185, 240, 0.1);
border-left: 4px solid #6bb9f0; border-left: 4px solid #6bb9f0;
border-radius: 8px;
min-height: 60px; min-height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
clip-path: var(--game-clip-path-sm);
} }
.combat-log p { .combat-log p {
@@ -3264,11 +3242,11 @@ body.no-scroll {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: bold; font-weight: bold;
border: none; border: none;
border-radius: 12px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
clip-path: var(--game-clip-path-sm);
} }
.combat-action-btn:disabled { .combat-action-btn:disabled {
@@ -3340,8 +3318,8 @@ body.no-scroll {
padding: 2rem; padding: 2rem;
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1), rgba(139, 0, 0, 0.1)); background: linear-gradient(135deg, rgba(244, 67, 54, 0.1), rgba(139, 0, 0, 0.1));
border: 2px solid rgba(244, 67, 54, 0.5); border: 2px solid rgba(244, 67, 54, 0.5);
border-radius: 12px;
animation: slideUp 0.3s ease-out; animation: slideUp 0.3s ease-out;
clip-path: var(--game-clip-path);
} }
.combat-header-inline { .combat-header-inline {
@@ -3367,10 +3345,10 @@ body.no-scroll {
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
aspect-ratio: 10 / 7; aspect-ratio: 10 / 7;
border-radius: 12px;
overflow: hidden; overflow: hidden;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border: 3px solid rgba(244, 67, 54, 0.5); border: 3px solid rgba(244, 67, 54, 0.5);
clip-path: var(--game-clip-path);
} }
.combat-enemy-image-large img { .combat-enemy-image-large img {
@@ -3427,11 +3405,11 @@ body.no-scroll {
width: 100%; width: 100%;
height: 32px; height: 32px;
background: rgba(20, 20, 20, 0.8); background: rgba(20, 20, 20, 0.8);
border-radius: 16px;
overflow: hidden; overflow: hidden;
border: 2px solid rgba(255, 100, 100, 0.6); border: 2px solid rgba(255, 100, 100, 0.6);
position: relative; position: relative;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
clip-path: var(--game-clip-path-sm);
} }
.combat-hp-fill-inline { .combat-hp-fill-inline {
@@ -3454,13 +3432,13 @@ body.no-scroll {
.combat-log-inline { .combat-log-inline {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(244, 67, 54, 0.3); border: 2px solid rgba(244, 67, 54, 0.3);
border-radius: 8px;
padding: 1rem; padding: 1rem;
text-align: center; text-align: center;
min-height: 60px; min-height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
clip-path: var(--game-clip-path-sm);
} }
.combat-log-inline p { .combat-log-inline p {
@@ -3475,13 +3453,13 @@ body.no-scroll {
font-size: 1.3rem; font-size: 1.3rem;
font-weight: bold; font-weight: bold;
padding: 0.75rem; padding: 0.75rem;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border: 2px solid transparent; border: 2px solid transparent;
min-height: 3rem; min-height: 3rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
clip-path: var(--game-clip-path-sm);
} }
/* Apply pulsing animation when it's enemy's turn processing */ /* Apply pulsing animation when it's enemy's turn processing */
@@ -3533,11 +3511,11 @@ body.no-scroll {
margin-top: 2rem; margin-top: 2rem;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(244, 67, 54, 0.3); border: 2px solid rgba(244, 67, 54, 0.3);
border-radius: 8px;
padding: 1rem; padding: 1rem;
max-width: 800px; max-width: 800px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
clip-path: var(--game-clip-path-sm);
} }
.combat-log-container h4 { .combat-log-container h4 {
@@ -3563,13 +3541,13 @@ body.no-scroll {
padding: 0.75rem; padding: 0.75rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-left: 3px solid rgba(244, 67, 54, 0.5); border-left: 3px solid rgba(244, 67, 54, 0.5);
border-radius: 4px;
line-height: 1.5; line-height: 1.5;
font-size: 0.95rem; font-size: 0.95rem;
animation: fadeInLog 0.3s ease; animation: fadeInLog 0.3s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
clip-path: var(--game-clip-path-sm);
} }
.combat-log-entry.player-action { .combat-log-entry.player-action {
@@ -3671,7 +3649,6 @@ body.no-scroll {
.location-description-box { .location-description-box {
background: rgba(25, 26, 31, 0.6); background: rgba(25, 26, 31, 0.6);
border: 1px solid rgba(107, 185, 240, 0.3); border: 1px solid rgba(107, 185, 240, 0.3);
border-radius: 8px;
padding: 1rem; padding: 1rem;
margin-top: 1rem; margin-top: 1rem;
width: 100%; width: 100%;
@@ -3679,6 +3656,7 @@ body.no-scroll {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
box-sizing: border-box; box-sizing: border-box;
clip-path: var(--game-clip-path);
} }
.location-description { .location-description {
@@ -3751,13 +3729,13 @@ body.no-scroll {
background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%); background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
color: white; color: white;
border: 1px solid rgba(255, 68, 68, 0.5); border: 1px solid rgba(255, 68, 68, 0.5);
border-radius: 6px;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
clip-path: var(--game-clip-path-sm);
} }
.pvp-btn:hover { .pvp-btn:hover {
@@ -3795,14 +3773,31 @@ body.no-scroll {
flex-wrap: wrap; flex-wrap: wrap;
} }
.open-inventory-btn {
width: 100%;
padding: 0.5rem 1rem;
border: none;
background: rgba(22, 33, 62, 0.3);
color: #fff;
cursor: pointer;
transition: all 0.3s;
font-size: 0.9rem;
clip-path: var(--game-clip-path-sm);
}
.open-inventory-btn:hover {
background: rgba(107, 185, 240, 0.2);
transform: translateY(-2px);
}
.pvp-player-card { .pvp-player-card {
background: rgba(30, 30, 40, 0.8); background: rgba(30, 30, 40, 0.8);
border: 2px solid rgba(255, 107, 107, 0.4); border: 2px solid rgba(255, 107, 107, 0.4);
border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
min-width: 280px; min-width: 280px;
flex: 1; flex: 1;
max-width: 400px; max-width: 400px;
clip-path: var(--game-clip-path);
} }
.pvp-player-card.your-card { .pvp-player-card.your-card {
@@ -4318,8 +4313,7 @@ body.no-scroll {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
min-height: 220px; /* min-height removed to fit content */
/* Reserve space for grid to prevent layout shift */
justify-content: center; justify-content: center;
/* Center content vertically */ /* Center content vertically */
} }
@@ -4330,7 +4324,7 @@ body.no-scroll {
gap: 1rem; gap: 1rem;
} }
.combat-actions-group .btn { .combat-actions .btn {
padding: 1rem; padding: 1rem;
font-size: 1.1rem; font-size: 1.1rem;
display: flex; display: flex;
@@ -4398,7 +4392,7 @@ body.no-scroll {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
position: relative; position: relative;
border-radius: 6px; clip-path: var(--game-clip-path-sm);
overflow: visible; overflow: visible;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
} }
@@ -4505,7 +4499,7 @@ body.no-scroll {
gap: 2px; gap: 2px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
padding: 2px; padding: 2px;
border-radius: 6px; clip-path: var(--game-clip-path-sm);
border: 1px solid rgba(72, 187, 120, 0.4); border: 1px solid rgba(72, 187, 120, 0.4);
/* Green border */ /* Green border */
align-items: center; align-items: center;
@@ -4519,7 +4513,6 @@ body.no-scroll {
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
@@ -4540,3 +4533,32 @@ body.no-scroll {
.action-btn.pickup.single:hover { .action-btn.pickup.single:hover {
background: rgba(72, 187, 120, 0.2); background: rgba(72, 187, 120, 0.2);
} }
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
}
.shake {
animation: shake 0.5s ease-in-out !important;
border-color: #ff4444 !important;
box-shadow: 0 0 10px rgba(255, 68, 68, 0.5) !important;
display: flex !important;
/* Ensure it stays flex */
transform-origin: center;
}

View File

@@ -457,6 +457,7 @@ function Game() {
onCraft={async (itemId: number) => await actions.handleCraft(itemId.toString())} onCraft={async (itemId: number) => await actions.handleCraft(itemId.toString())}
onRepair={(uniqueItemId: string, inventoryId: number) => actions.handleRepairFromMenu(Number(uniqueItemId), inventoryId)} onRepair={(uniqueItemId: string, inventoryId: number) => actions.handleRepairFromMenu(Number(uniqueItemId), inventoryId)}
onUncraft={(uniqueItemId: string, inventoryId: number) => actions.handleUncraft(Number(uniqueItemId), inventoryId)} onUncraft={(uniqueItemId: string, inventoryId: number) => actions.handleUncraft(Number(uniqueItemId), inventoryId)}
failedActionItemId={state.failedActionItemId}
/> />
)} )}
</div> </div>

View File

@@ -32,7 +32,7 @@
background: linear-gradient(135deg, rgba(225, 29, 72, 0.1) 0%, transparent 100%); background: linear-gradient(135deg, rgba(225, 29, 72, 0.1) 0%, transparent 100%);
border: 1px solid rgba(225, 29, 72, 0.3); border: 1px solid rgba(225, 29, 72, 0.3);
/* Angled Cut */ /* Angled Cut */
clip-path: polygon(15px 0, 100% 0, 100% 100%, 0 100%, 0 15px); clip-path: var(--game-clip-path);
border-left: 3px solid var(--game-color-primary); border-left: 3px solid var(--game-color-primary);
position: relative; position: relative;
} }
@@ -85,7 +85,7 @@
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
/* Tech Shape: Angled top-left and bottom-right */ /* Tech Shape: Angled top-left and bottom-right */
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px); clip-path: var(--game-clip-path);
position: relative; position: relative;
} }
@@ -132,7 +132,7 @@
padding: 0 12px; padding: 0 12px;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(76, 209, 55, 0.3); border: 1px solid rgba(76, 209, 55, 0.3);
clip-path: polygon(8px 0, 100% 0, 100% 100%, 0 100%, 0 8px); clip-path: var(--game-clip-path-sm);
font-size: 0.8rem; font-size: 0.8rem;
color: #aaddaa; color: #aaddaa;
font-family: monospace; font-family: monospace;
@@ -177,7 +177,7 @@
font-weight: 700; font-weight: 700;
transition: all 0.2s; transition: all 0.2s;
/* Same tech shape */ /* Same tech shape */
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px); clip-path: var(--game-clip-path-sm);
text-transform: uppercase; text-transform: uppercase;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -209,7 +209,7 @@
font-size: 1rem; font-size: 1rem;
transition: all 0.2s; transition: all 0.2s;
/* Angled corners */ /* Angled corners */
clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px); clip-path: var(--game-clip-path-sm);
} }
.header-icon-btn:hover { .header-icon-btn:hover {
@@ -223,7 +223,7 @@
.game-header .language-btn { .game-header .language-btn {
height: 36px; height: 36px;
border-radius: 0; border-radius: 0;
clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px); clip-path: var(--game-clip-path-sm);
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.15);
} }
@@ -237,7 +237,7 @@
border-radius: 0; border-radius: 0;
border: 1px solid var(--game-border-color); border: 1px solid var(--game-border-color);
background: rgba(10, 10, 15, 0.95); background: rgba(10, 10, 15, 0.95);
clip-path: polygon(0 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%); clip-path: var(--game-clip-path);
} }
/* Separator line */ /* Separator line */

View File

@@ -0,0 +1,117 @@
.game-btn {
border: none;
color: white;
cursor: pointer;
font-weight: 600;
font-family: var(--game-font-main);
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
clip-path: var(--game-clip-path-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
.game-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
filter: grayscale(0.8);
}
.game-btn:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.game-btn:not(:disabled):active {
transform: translateY(0);
}
/* Sizes */
.game-btn.sm {
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
}
.game-btn.md {
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
.game-btn.lg {
padding: 0.8rem 1.5rem;
font-size: 1rem;
}
/* Variants */
/* Primary - Blue (Default) */
.game-btn.primary {
background: linear-gradient(135deg, #6bb9f0, #89d4ff);
box-shadow: 0 2px 8px rgba(107, 185, 240, 0.3);
}
.game-btn.primary:not(:disabled):hover {
background: linear-gradient(135deg, #89d4ff, #6bb9f0);
box-shadow: 0 4px 12px rgba(107, 185, 240, 0.5);
}
/* Secondary - Grey/Dark */
.game-btn.secondary {
background: linear-gradient(135deg, #4a5568, #718096);
box-shadow: 0 2px 8px rgba(74, 85, 104, 0.3);
color: #e2e8f0;
}
.game-btn.secondary:not(:disabled):hover {
background: linear-gradient(135deg, #718096, #4a5568);
box-shadow: 0 4px 12px rgba(74, 85, 104, 0.5);
}
/* Success - Green */
.game-btn.success {
background: linear-gradient(135deg, #4caf50, #66bb6a);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.game-btn.success:not(:disabled):hover {
background: linear-gradient(135deg, #66bb6a, #4caf50);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.5);
}
/* Danger - Red */
.game-btn.danger {
background: linear-gradient(135deg, #f44336, #ef5350);
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.3);
}
.game-btn.danger:not(:disabled):hover {
background: linear-gradient(135deg, #ef5350, #f44336);
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.5);
}
/* Info - Cyan/Teal (Equip/Unequip often uses this) */
.game-btn.info {
background: linear-gradient(135deg, #00bcd4, #26c6da);
box-shadow: 0 2px 8px rgba(0, 188, 212, 0.3);
}
.game-btn.info:not(:disabled):hover {
background: linear-gradient(135deg, #26c6da, #00bcd4);
box-shadow: 0 4px 12px rgba(0, 188, 212, 0.5);
}
/* Warning - Orange/Yellow */
.game-btn.warning {
background: linear-gradient(135deg, #ff9800, #ffa726);
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3);
}
.game-btn.warning:not(:disabled):hover {
background: linear-gradient(135deg, #ffa726, #ff9800);
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.5);
}

View File

@@ -0,0 +1,38 @@
import React, { ReactNode } from 'react';
import './GameButton.css';
interface GameButtonProps {
children: ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'info' | 'warning';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
}
export const GameButton: React.FC<GameButtonProps> = ({
children,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
className = '',
style
}) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (disabled) return;
if (onClick) onClick(e);
};
return (
<button
className={`game-btn ${variant} ${size} ${className}`}
onClick={handleClick}
disabled={disabled}
style={style}
>
{children}
</button>
);
};

View File

@@ -0,0 +1,97 @@
.game-dropdown-menu {
position: fixed;
z-index: 10000;
background: var(--game-bg-panel, #1a202c);
border: 1px solid var(--game-border-color, #4a5568);
box-shadow: var(--game-shadow-modal, 0 10px 15px -3px rgba(0, 0, 0, 0.5));
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
clip-path: var(--game-clip-path);
animation: dropdownFadeIn 0.1s ease-out;
max-height: 300px;
overflow-y: auto;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.8rem;
background: transparent;
border: none;
color: var(--game-text-secondary, #a0aec0);
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
text-align: left;
transition: all 0.2s;
border-radius: 4px;
/* Optional if not using clip-path on items */
width: 100%;
}
.game-dropdown-item:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--game-text-primary, #fff);
}
.game-dropdown-item:disabled {
opacity: 0.5;
cursor: not-allowed;
background: transparent;
}
.game-dropdown-item.danger {
color: #f56565;
}
.game-dropdown-item.danger:hover {
background: rgba(245, 101, 101, 0.15);
}
.game-dropdown-item.success {
color: #48bb78;
}
.game-dropdown-item.success:hover {
background: rgba(72, 187, 120, 0.15);
}
.game-dropdown-item.info {
color: #4299e1;
}
.game-dropdown-item.info:hover {
background: rgba(66, 153, 225, 0.15);
}
.game-dropdown-divider {
height: 1px;
background: var(--game-border-color, #4a5568);
margin: 0.25rem 0;
}
.game-dropdown-header {
padding: 0.5rem 0.8rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--game-text-muted, #718096);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--game-border-color, #4a5568);
margin-bottom: 0.25rem;
}

View File

@@ -0,0 +1,99 @@
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import './GameDropdown.css';
interface GameDropdownProps {
isOpen: boolean;
onClose: () => void;
position: { x: number; y: number };
children: React.ReactNode;
className?: string;
width?: string;
}
/**
* GameDropdown
*
* A reusable dropdown component that renders outside the DOM hierarchy
* to avoid z-index/overflow issues.
* Closes when clicking outside.
*/
export const GameDropdown: React.FC<GameDropdownProps> = ({
isOpen,
onClose,
position,
children,
className = '',
width = '200px'
}) => {
const dropdownRef = useRef<HTMLDivElement>(null);
// Handle click outside
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
onClose();
}
};
// Use mousedown to catch clicks before they might trigger other things
document.addEventListener('mousedown', handleClickOutside);
// Handle scroll to update position or close?
// For now, simpler to just close on scroll or let it float (fixed pos)
const handleScroll = () => {
// Optional: onClose();
};
window.addEventListener('scroll', handleScroll, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', handleScroll, true);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
// Adjust position to keep within viewport
let { x, y } = position;
// Simple adjustment logic (can be improved with measuring ref)
// We'll trust the parent passed reasonable coords, but ensure it doesn't go off-screen right/bottom
if (typeof window !== 'undefined') {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Approximate width if not measured yet (or use min-width)
const estimatedWidth = parseInt(width) || 200;
const estimatedHeight = 200; // Guess for now
if (x + estimatedWidth > viewportWidth) {
x = viewportWidth - estimatedWidth - 10;
}
if (y + estimatedHeight > viewportHeight) {
// Flip up if space
y = y - estimatedHeight;
// Or just limit to bottom
if (y < 0) y = 10;
}
}
return createPortal(
<div
ref={dropdownRef}
className={`game-dropdown-menu ${className}`}
style={{
top: y,
left: x,
width: width
}}
onClick={(e) => e.stopPropagation()} // Prevent clicks inside from closing
>
{children}
</div>,
document.body
);
};

View File

@@ -73,7 +73,7 @@ export const GameTooltip: React.FC<GameTooltipProps> = ({ content, children, cla
<div style={{ <div style={{
background: 'var(--game-bg-tooltip, #151515)', background: 'var(--game-bg-tooltip, #151515)',
border: '1px solid var(--game-border-color, #333)', border: '1px solid var(--game-border-color, #333)',
borderRadius: 'var(--game-radius-sm, 4px)', clipPath: 'var(--game-clip-path)',
padding: '0.5rem 0.8rem', padding: '0.5rem 0.8rem',
boxShadow: 'var(--game-shadow-tooltip, 0 4px 12px rgba(0,0,0,0.5))', boxShadow: 'var(--game-shadow-tooltip, 0 4px 12px rgba(0,0,0,0.5))',
color: 'var(--game-text-primary, #ddd)', color: 'var(--game-text-primary, #ddd)',

View File

@@ -248,18 +248,18 @@ export const Combat: React.FC<CombatProps> = ({
} }
} }
// Handle combat_over from WebSocket // Handle combat_over from WebSocket or initial state (including fled states which might not set combat_over flag explicitly in all paths)
if (initialCombatData?.combat_over) { const pvp = initialCombatData?.pvp_combat;
const pvp = initialCombatData?.pvp_combat; const myId = pvp?.is_attacker
const myId = pvp?.is_attacker ? pvp?.attacker?.id
? pvp?.attacker?.id : pvp?.defender?.id;
: pvp?.defender?.id;
// Check if someone fled // Check if someone fled (Robust check for both combat_over=true AND implicit state)
const iAmAttacker = pvp?.is_attacker; const iAmAttacker = pvp?.is_attacker;
const opponentFled = iAmAttacker ? pvp?.defender_fled : pvp?.attacker_fled; const opponentFled = iAmAttacker ? pvp?.defender_fled : pvp?.attacker_fled;
const iFled = iAmAttacker ? pvp?.attacker_fled : pvp?.defender_fled; const iFled = iAmAttacker ? pvp?.attacker_fled : pvp?.defender_fled;
if (initialCombatData?.combat_over || opponentFled || iFled) {
if (opponentFled) { if (opponentFled) {
// Opponent fled - I "win" by default // Opponent fled - I "win" by default
setCombatResult('victory'); setCombatResult('victory');

View File

@@ -7,11 +7,11 @@
/* More transparent/themed background */ /* More transparent/themed background */
background: rgba(20, 20, 20, 0.6); background: rgba(20, 20, 20, 0.6);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
border-radius: 12px;
padding: 1rem; padding: 1rem;
color: white; color: white;
position: relative; position: relative;
overflow: hidden; /* overflow: hidden; Removed to allow floating text to be seen */
clip-path: var(--game-clip-path);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
@@ -118,10 +118,10 @@
width: 100%; width: 100%;
height: 16px; height: 16px;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-top: 5px; margin-top: 5px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
clip-path: var(--game-clip-path-sm);
} }
.health-bar-fill { .health-bar-fill {
@@ -197,12 +197,12 @@
/* Combat Log */ /* Combat Log */
.combat-log-container { .combat-log-container {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 8px;
padding: 0.5rem; padding: 0.5rem;
margin-top: 1rem; margin-top: 1rem;
height: 150px; height: 150px;
overflow-y: auto; overflow-y: auto;
font-size: 0.9rem; font-size: 0.9rem;
clip-path: var(--game-clip-path-sm);
} }
.log-message { .log-message {
@@ -236,7 +236,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 1000;
} }
.floating-text { .floating-text {
@@ -246,6 +246,8 @@
animation: float-up 5s forwards; animation: float-up 5s forwards;
pointer-events: none; pointer-events: none;
text-shadow: 2px 2px 0 #000; text-shadow: 2px 2px 0 #000;
z-index: 2000;
/* Ensure on top of everything */
} }
.type-damage { .type-damage {
@@ -346,10 +348,10 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
padding: 1rem 2rem; padding: 1rem 2rem;
border-radius: 20px;
font-size: 1.5rem; font-size: 1.5rem;
animation: pulse 1s infinite; animation: pulse 1s infinite;
z-index: 20; z-index: 20;
clip-path: var(--game-clip-path-sm);
} }
@keyframes pulse { @keyframes pulse {
@@ -419,10 +421,10 @@
.stat-block { .stat-block {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
position: relative; position: relative;
clip-path: var(--game-clip-path-sm);
} }
.stat-block.enemy { .stat-block.enemy {
@@ -463,13 +465,12 @@
height: 12px; height: 12px;
/* Slightly thinner than header */ /* Slightly thinner than header */
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
border-radius: 6px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
clip-path: var(--game-clip-path-sm);
} }
.progress-fill { .progress-fill {
height: 100%; height: 100%;
border-radius: 6px;
transition: width 0.3s ease-out; transition: width 0.3s ease-out;
} }

View File

@@ -78,7 +78,7 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border: 1px solid #3a4b5c; border: 1px solid #3a4b5c;
border-radius: 8px; clip-path: var(--game-clip-path-sm);
color: #fff; color: #fff;
font-size: 1rem; font-size: 1rem;
outline: none; outline: none;
@@ -118,7 +118,7 @@
flex-direction: row; flex-direction: row;
background-color: rgba(26, 32, 44, 0.8); background-color: rgba(26, 32, 44, 0.8);
border: 1px solid #2d3748; border: 1px solid #2d3748;
border-radius: 0.5rem; clip-path: var(--game-clip-path);
padding: 0.75rem; padding: 0.75rem;
gap: 1rem; gap: 1rem;
align-items: stretch; align-items: stretch;
@@ -142,7 +142,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: 6px; clip-path: var(--game-clip-path);
border: 1px solid #4a5568; border: 1px solid #4a5568;
} }
@@ -172,7 +172,7 @@
color: #fff; color: #fff;
font-size: 0.75rem; font-size: 0.75rem;
padding: 2px 6px; padding: 2px 6px;
border-radius: 10px; clip-path: var(--game-clip-path-sm);
font-weight: bold; font-weight: bold;
} }
@@ -217,7 +217,7 @@
.stat-badge { .stat-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 0.375rem; clip-path: var(--game-clip-path-sm);
border: 1px solid; border: 1px solid;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -305,7 +305,7 @@
color: #48bb78; color: #48bb78;
border: 1px solid rgba(72, 187, 120, 0.4); border: 1px solid rgba(72, 187, 120, 0.4);
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 6px; clip-path: var(--game-clip-path-sm);
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: bold;
margin-left: 0.5rem; margin-left: 0.5rem;

View File

@@ -156,17 +156,46 @@ export const CombatView: React.FC<CombatViewProps> = ({
</div> </div>
{/* 2. HP Bars (Character Sheet Style) - Staggered Lines */} {/* 2. HP Bars (Character Sheet Style) - Staggered Lines */}
<div className="combat-stats-container"> <div className="combat-stats-container" style={{ position: 'relative' }}>
{/* Floating Text - Enemy (Left 60%) */}
<div style={{
position: 'absolute',
top: '20px',
left: 0,
width: '60%',
display: 'flex',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 2000
}}>
{floatingTexts.filter(ft => ft.origin === 'enemy').map(ft => (
<div key={ft.id} className={`floating-text type-${ft.type}`} style={{ position: 'absolute' }}>
{ft.text}
</div>
))}
</div>
{/* Floating Text - Player (Right 60%) */}
<div style={{
position: 'absolute',
bottom: '20px',
right: 0,
width: '60%',
display: 'flex',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 2000
}}>
{floatingTexts.filter(ft => ft.origin === 'player').map(ft => (
<div key={ft.id} className={`floating-text type-${ft.type}`} style={{ position: 'absolute' }}>
{ft.text}
</div>
))}
</div>
{/* Enemy HP (Left) */} {/* Enemy HP (Left) */}
<div className={`stat-block enemy ${animState.npcHit ? 'shake-effect' : ''}`}> <div className={`stat-block enemy ${animState.npcHit ? 'shake-effect' : ''}`}>
<div className="floating-text-layer" style={{ height: '0', overflow: 'visible' }}>
{floatingTexts.filter(ft => ft.origin === 'enemy').map(ft => (
<div key={ft.id} className={`floating-text type-${ft.type}`} style={{ left: `${ft.x}%`, top: `${ft.y - 50}%` }}>
{ft.text}
</div>
))}
</div>
<GameProgressBar <GameProgressBar
label={state.npcName || t('common.enemy')} label={state.npcName || t('common.enemy')}
value={state.npcHp} value={state.npcHp}
@@ -180,13 +209,6 @@ export const CombatView: React.FC<CombatViewProps> = ({
{/* Player HP (Right) */} {/* Player HP (Right) */}
<div className={`stat-block player ${animState.playerHit ? 'shake-effect flash-hit' : ''}`}> <div className={`stat-block player ${animState.playerHit ? 'shake-effect flash-hit' : ''}`}>
<div className="floating-text-layer" style={{ height: '0', overflow: 'visible' }}>
{floatingTexts.filter(ft => ft.origin === 'player').map(ft => (
<div key={ft.id} className={`floating-text type-${ft.type}`} style={{ left: `${ft.x}%`, top: `${ft.y - 50}%` }}>
{ft.text}
</div>
))}
</div>
<GameProgressBar <GameProgressBar
label={playerName || t('common.you')} label={playerName || t('common.you')}
value={state.playerHp} value={state.playerHp}

View File

@@ -104,7 +104,7 @@
.metric-fill { .metric-fill {
height: 100%; height: 100%;
border-radius: var(--game-radius-sm); clip-path: var(--game-clip-path-sm);
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@@ -191,7 +191,8 @@
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 8px; border: 1px solid transparent;
clip-path: var(--game-clip-path-sm);
color: #a0aec0; color: #a0aec0;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -231,12 +232,12 @@
background: var(--game-bg-app); background: var(--game-bg-app);
} }
.inventory-search-bar { .game-search-container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
background: var(--game-bg-input); background: var(--game-bg-input, rgba(0, 0, 0, 0.3));
border: 1px solid var(--game-border-color); border: 1px solid var(--game-border-color);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: var(--game-text-primary); color: var(--game-text-primary);
@@ -245,7 +246,12 @@
clip-path: var(--game-clip-path-sm); clip-path: var(--game-clip-path-sm);
} }
.inventory-search-bar input { .game-search-icon {
font-size: 1rem;
opacity: 0.7;
}
.game-search-input {
background: transparent; background: transparent;
border: none; border: none;
color: #fff; color: #fff;
@@ -254,6 +260,41 @@
outline: none; outline: none;
} }
/* View Toggle Button */
.inventory-view-toggle {
display: flex;
align-items: center;
border-left: 1px solid var(--game-border-color);
padding-left: 0.75rem;
margin-left: 0.5rem;
}
.view-toggle-btn {
background: transparent;
border: 1px solid transparent;
color: #a0aec0;
cursor: pointer;
font-size: 1.2rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
/* Minor radius for small button */
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.view-toggle-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.view-toggle-btn.active {
color: #63b3ed;
background: rgba(66, 153, 225, 0.15);
border-color: rgba(66, 153, 225, 0.3);
}
.inventory-items-grid { .inventory-items-grid {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@@ -263,6 +304,30 @@
padding-right: 0.5rem; padding-right: 0.5rem;
} }
/* Grid View Layout */
/* Grid View Layout */
.items-container.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
grid-auto-rows: max-content;
gap: 1rem;
align-content: start;
}
.items-container.list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Wrapper to handle tooltip + card relative positioning */
.inventory-grid-wrapper {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
}
/* Compact Item Card */ /* Compact Item Card */
/* Compact Item Card */ /* Compact Item Card */
.inventory-item-card.compact { .inventory-item-card.compact {
@@ -270,7 +335,7 @@
flex-direction: row; flex-direction: row;
background-color: var(--game-bg-card); background-color: var(--game-bg-card);
border: 1px solid var(--game-border-color); border: 1px solid var(--game-border-color);
border-radius: var(--game-radius-md); clip-path: var(--game-clip-path);
padding: 0.75rem; padding: 0.75rem;
gap: 1rem; gap: 1rem;
align-items: stretch; align-items: stretch;
@@ -285,6 +350,45 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Base Card Style for Grid */
.inventory-item-card.grid {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--game-bg-card);
border: 1px solid var(--game-border-color);
clip-path: var(--game-clip-path);
padding: 0.5rem;
aspect-ratio: 1;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--game-shadow-sm);
}
.inventory-item-card.grid:hover,
.inventory-item-card.grid.active {
border-color: #63b3ed;
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
z-index: 10;
}
.inventory-item-card.grid.equipped {
border-color: #63b3ed;
background: rgba(66, 153, 225, 0.1);
}
/* Grid Image Area */
.item-grid-image {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.25rem;
}
.item-image-section.small { .item-image-section.small {
width: 100px; width: 100px;
height: 100px; height: 100px;
@@ -294,7 +398,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: 6px; clip-path: var(--game-clip-path-no-br);
border: 1px solid #4a5568; border: 1px solid #4a5568;
margin: auto; margin: auto;
} }
@@ -312,6 +416,11 @@
justify-content: center; justify-content: center;
} }
/* Grid Icon adjustments */
.inventory-item-card.grid .item-icon-large {
font-size: 2.5rem;
}
.item-icon-large.hidden { .item-icon-large.hidden {
display: none; display: none;
} }
@@ -325,11 +434,32 @@
color: var(--game-text-primary); color: var(--game-text-primary);
font-size: 0.75rem; font-size: 0.75rem;
padding: 2px 6px; padding: 2px 6px;
border-radius: var(--game-radius-sm); clip-path: var(--game-clip-path-sm);
font-weight: bold; font-weight: bold;
box-shadow: var(--game-shadow-sm); box-shadow: var(--game-shadow-sm);
} }
/* Position adjustment for grid view badge */
.inventory-item-card.grid .item-quantity-badge {
bottom: 2px;
right: 2px;
font-size: 0.7rem;
padding: 1px 4px;
}
.item-equipped-indicator {
position: absolute;
top: 2px;
right: 2px;
background: #4299e1;
color: #fff;
font-size: 0.65rem;
font-weight: bold;
padding: 1px 4px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.item-info-section { .item-info-section {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -374,7 +504,7 @@
.item-tier-badge { .item-tier-badge {
font-size: 0.7rem; font-size: 0.7rem;
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; clip-path: var(--game-clip-path-sm);
background: #4a5568; background: #4a5568;
color: #e2e8f0; color: #e2e8f0;
font-weight: bold; font-weight: bold;
@@ -510,7 +640,7 @@
.stat-badge { .stat-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 0.375rem; clip-path: var(--game-clip-path-sm);
border: 1px solid; border: 1px solid;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -611,7 +741,8 @@
.durability-track { .durability-track {
height: 0.5rem; height: 0.5rem;
background-color: #374151; background-color: #374151;
border-radius: 9999px; background-color: #374151;
clip-path: var(--game-clip-path-sm);
overflow: hidden; overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
@@ -661,6 +792,9 @@
letter-spacing: 0.05em; letter-spacing: 0.05em;
border-bottom: 1px solid #4a5568; border-bottom: 1px solid #4a5568;
margin: 0.5rem 0; margin: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.item-actions-section.bottom-right { .item-actions-section.bottom-right {
@@ -671,13 +805,17 @@
.action-btn { .action-btn {
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
border: none; border: 1px solid var(--game-border-color);
border-radius: 6px; background: rgba(255, 255, 255, 0.05);
clip-path: var(--game-clip-path-sm);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
display: inline-flex;
align-items: center;
justify-content: center;
} }
.action-btn:disabled, .action-btn:disabled,
@@ -730,7 +868,7 @@
gap: 2px; gap: 2px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
padding: 2px; padding: 2px;
border-radius: 6px; clip-path: var(--game-clip-path-sm);
border: 1px solid rgba(245, 101, 101, 0.3); border: 1px solid rgba(245, 101, 101, 0.3);
} }
@@ -740,7 +878,7 @@
border: none; border: none;
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
font-size: 0.75rem; font-size: 0.75rem;
border-radius: 4px; clip-path: var(--game-clip-path-sm);
} }
.action-btn.drop:hover { .action-btn.drop:hover {
@@ -772,7 +910,7 @@
.item-card-equipped { .item-card-equipped {
font-size: 0.7rem; font-size: 0.7rem;
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; clip-path: var(--game-clip-path-sm);
background: rgba(66, 153, 225, 0.2); background: rgba(66, 153, 225, 0.2);
color: #63b3ed; color: #63b3ed;
border: 1px solid rgba(66, 153, 225, 0.4); border: 1px solid rgba(66, 153, 225, 0.4);
@@ -793,31 +931,41 @@
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.subcategory-header { /* Tooltip Custom Content */
.item-tooltip-content {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
padding: 0.4rem 0.75rem; min-width: 180px;
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid #4299e1;
margin: 0.5rem 0;
border-radius: 0 4px 4px 0;
} }
.subcat-icon { .tooltip-header {
font-size: 1rem; font-weight: 700;
font-size: 0.95rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 0.25rem;
margin-bottom: 0.25rem;
} }
.subcat-label { .tooltip-desc {
font-size: 0.8rem;
font-weight: 500;
color: #a0aec0; color: #a0aec0;
text-transform: uppercase; font-size: 0.85rem;
letter-spacing: 0.03em; font-style: italic;
line-height: 1.3;
} }
.subcat-count { .tooltip-stats {
font-size: 0.75rem; display: flex;
color: #718096; gap: 1rem;
margin-left: auto; font-size: 0.8rem;
color: #cbd5e0;
background: rgba(0, 0, 0, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.tooltip-stat {
font-size: 0.85rem;
font-weight: 500;
color: #e2e8f0;
} }

View File

@@ -1,4 +1,4 @@
import { MouseEvent, ChangeEvent, useEffect } from 'react' import { MouseEvent, ChangeEvent, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAudio } from '../../contexts/AudioContext' import { useAudio } from '../../contexts/AudioContext'
import { PlayerState, Profile, Equipment } from './types' import { PlayerState, Profile, Equipment } from './types'
@@ -7,6 +7,9 @@ import { getTranslatedText } from '../../utils/i18nUtils'
import './InventoryModal.css' import './InventoryModal.css'
import { EffectBadge } from './EffectBadge' import { EffectBadge } from './EffectBadge'
import { GameTooltip } from '../common/GameTooltip' import { GameTooltip } from '../common/GameTooltip'
import { GameDropdown } from '../common/GameDropdown'
import { GameButton } from '../common/GameButton'
import '../common/GameDropdown.css'
interface InventoryModalProps { interface InventoryModalProps {
playerState: PlayerState playerState: PlayerState
@@ -40,6 +43,15 @@ function InventoryModal({
const { t } = useTranslation() const { t } = useTranslation()
const { playSfx } = useAudio() const { playSfx } = useAudio()
// View Mode State
const [viewMode, setViewMode] = useState<'list' | 'grid'>(() => {
return (localStorage.getItem('inventoryViewMode') as 'list' | 'grid') || 'list';
});
// Dropdown State
const [activeDropdown, setActiveDropdown] = useState<number | null>(null); // Uses item.id (inventory ID)
const [dropdownPos, setDropdownPos] = useState({ x: 0, y: 0 });
// Play sound on mount // Play sound on mount
useEffect(() => { useEffect(() => {
playSfx('/audio/sfx/inventory_open.wav') playSfx('/audio/sfx/inventory_open.wav')
@@ -49,6 +61,26 @@ function InventoryModal({
// But for "close" button click we can play it. // But for "close" button click we can play it.
}, []) }, [])
const toggleViewMode = () => {
const newMode = viewMode === 'list' ? 'grid' : 'list';
setViewMode(newMode);
localStorage.setItem('inventoryViewMode', newMode);
playSfx('/audio/sfx/click.wav'); // Or a generic interaction sound
};
const handleItemClick = (e: MouseEvent, item: any) => {
if (viewMode === 'grid') {
e.stopPropagation();
// Toggle dropdown
if (activeDropdown === item.id) {
setActiveDropdown(null);
} else {
setDropdownPos({ x: e.clientX, y: e.clientY });
setActiveDropdown(item.id);
}
}
};
const handleClose = () => { const handleClose = () => {
playSfx('/audio/sfx/inventory_close.wav') playSfx('/audio/sfx/inventory_close.wav')
onClose() onClose()
@@ -349,6 +381,312 @@ function InventoryModal({
) )
} }
const renderGridItem = (item: any, i: number) => {
// Prepare actions for this item
const statusEffect = item.effects?.status_effect;
const isEffectActive = statusEffect && playerState.status_effects.some((e: any) => {
const effectName = typeof e.effect_name === 'string' ? e.effect_name : e.effect_name['en'];
const itemName = typeof statusEffect.name === 'string' ? statusEffect.name : statusEffect.name['en'];
return effectName === itemName;
});
const maxDurability = item.max_durability;
const currentDurability = item.durability;
const hasDurability = maxDurability && maxDurability > 0;
const tooltipContent = (
<div className="item-tooltip-content">
<div className={`tooltip-header text-tier-${item.tier || 0}`}>
{item.emoji} {getTranslatedText(item.name)}
</div>
{item.description && <div className="tooltip-desc">{getTranslatedText(item.description)}</div>}
<div className="tooltip-stats">
<div> {item.weight}kg {item.quantity > 1 && `(x${item.quantity})`}</div>
<div>📦 {item.volume}L {item.quantity > 1 && `(x${item.quantity})`}</div>
</div>
{/* Stats Row - Button-like Badges */}
<div className="stat-badges-container">
{/* Capacity */}
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
<span className="stat-badge capacity">
+{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg
</span>
)}
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
<span className="stat-badge capacity">
📦 +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L
</span>
)}
{/* Combat */}
{(item.unique_stats?.damage_min || item.stats?.damage_min) && (
<span className="stat-badge damage">
{item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max}
</span>
)}
{(item.unique_stats?.armor || item.stats?.armor) && (
<span className="stat-badge armor">
🛡 +{item.unique_stats?.armor || item.stats?.armor}
</span>
)}
{(item.unique_stats?.armor_penetration || item.stats?.armor_penetration) && (
<span className="stat-badge penetration">
💔 +{item.unique_stats?.armor_penetration || item.stats?.armor_penetration} {t('stats.pen')}
</span>
)}
{(item.unique_stats?.crit_chance || item.stats?.crit_chance) && (
<span className="stat-badge crit">
🎯 +{Math.round((item.unique_stats?.crit_chance || item.stats?.crit_chance) * 100)}% {t('stats.crit')}
</span>
)}
{(item.unique_stats?.accuracy || item.stats?.accuracy) && (
<span className="stat-badge accuracy">
👁 +{Math.round((item.unique_stats?.accuracy || item.stats?.accuracy) * 100)}% {t('stats.acc')}
</span>
)}
{(item.unique_stats?.dodge_chance || item.stats?.dodge_chance) && (
<span className="stat-badge dodge">
💨 +{Math.round((item.unique_stats?.dodge_chance || item.stats?.dodge_chance) * 100)}% Dodge
</span>
)}
{(item.unique_stats?.lifesteal || item.stats?.lifesteal) && (
<span className="stat-badge lifesteal">
🧛 +{Math.round((item.unique_stats?.lifesteal || item.stats?.lifesteal) * 100)}% {t('stats.life')}
</span>
)}
{/* Attributes */}
{(item.unique_stats?.strength_bonus || item.stats?.strength_bonus) && (
<span className="stat-badge strength">
💪 +{item.unique_stats?.strength_bonus || item.stats?.strength_bonus} {t('stats.str')}
</span>
)}
{(item.unique_stats?.agility_bonus || item.stats?.agility_bonus) && (
<span className="stat-badge agility">
🏃 +{item.unique_stats?.agility_bonus || item.stats?.agility_bonus} {t('stats.agi')}
</span>
)}
{(item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus) && (
<span className="stat-badge endurance">
🏋 +{item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus} {t('stats.end')}
</span>
)}
{(item.unique_stats?.hp_bonus || item.stats?.hp_bonus) && (
<span className="stat-badge health">
+{item.unique_stats?.hp_bonus || item.stats?.hp_bonus} {t('stats.hpMax')}
</span>
)}
{(item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus) && (
<span className="stat-badge stamina">
+{item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus} {t('stats.stmMax')}
</span>
)}
{/* Consumables */}
{item.hp_restore && (
<span className="stat-badge health">
+{item.hp_restore} HP
</span>
)}
{item.stamina_restore && (
<span className="stat-badge stamina">
+{item.stamina_restore} Stm
</span>
)}
{/* Status Effects */}
{item.effects?.status_effect && (
<EffectBadge effect={item.effects.status_effect} />
)}
{item.effects?.cures && item.effects.cures.length > 0 && (
<span className="stat-badge cure">
💊 {t('game.cures')}: {item.effects.cures.map((c: string) => t(`game.effects.${c}`, c)).join(', ')}
</span>
)}
</div>
{/* Durability Bar */}
{hasDurability && (
<div className="durability-container">
<div className="durability-header">
<span>{t('game.durability')}</span>
<span className={
currentDurability < maxDurability * 0.2
? "durability-text-low"
: ""
}>
{currentDurability} / {maxDurability}
</span>
</div>
<div className="durability-track">
<div
className={`durability-fill ${currentDurability < maxDurability * 0.2
? "low"
: currentDurability < maxDurability * 0.5
? "medium"
: "high"
}`}
style={{
width: `${Math.min(100, Math.max(0, (currentDurability / maxDurability) * 100))}%`
}}
/>
</div>
</div>
)}
</div>
);
return (
<div key={i} className="inventory-grid-wrapper">
<GameTooltip content={tooltipContent}>
<div
className={`inventory-item-card grid ${item.is_equipped ? 'equipped' : ''} ${activeDropdown === item.id ? 'active' : ''} text-tier-${item.tier || 0}`}
onClick={(e) => handleItemClick(e, item)}
>
{/* Image/Icon */}
<div className="item-grid-image">
{item.image_path ? (
<img
src={getAssetPath(item.image_path)}
alt={getTranslatedText(item.name)}
className="item-img-thumb"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
const icon = (e.target as HTMLImageElement).nextElementSibling;
if (icon) icon.classList.remove('hidden');
}}
/>
) : null}
<div className={`item-icon-large ${item.tier ? `tier-${item.tier}` : ''} ${item.image_path ? 'hidden' : ''}`}>
{item.emoji || '📦'}
</div>
</div>
{/* Quantity Badge */}
{item.quantity > 1 && <div className="item-quantity-badge">x{item.quantity}</div>}
{/* Equipped Indicator */}
{item.is_equipped && <div className="item-equipped-indicator">E</div>}
</div>
</GameTooltip>
{/* Dropdown Menu */}
{activeDropdown === item.id && (
<GameDropdown
isOpen={true}
onClose={() => setActiveDropdown(null)}
position={dropdownPos}
width="180px"
>
<div className="game-dropdown-header">
{getTranslatedText(item.name)}
</div>
{item.consumable && (
<GameButton
variant="success"
size="sm"
disabled={isEffectActive}
onClick={() => {
if (!isEffectActive) {
playSfx('/audio/sfx/use.wav');
onUseItem(item.item_id, item.id);
setActiveDropdown(null);
}
}}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
{t('game.use')}
</GameButton>
)}
{item.equippable && !item.is_equipped && (
<GameButton
variant="info"
size="sm"
onClick={() => {
playSfx('/audio/sfx/equip.wav');
onEquipItem(item.id);
setActiveDropdown(null);
}}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
{t('game.equip')}
</GameButton>
)}
{item.is_equipped && (
<GameButton
variant="info"
size="sm"
onClick={() => {
playSfx('/audio/sfx/unequip.wav');
onUnequipItem(item.slot);
setActiveDropdown(null);
}}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
{t('game.unequip')}
</GameButton>
)}
{(item.consumable || (item.equippable && !item.is_equipped) || item.is_equipped) &&
<div className="game-dropdown-divider" />
}
<GameButton
variant="danger"
size="sm"
onClick={() => {
playSfx('/audio/sfx/drop.wav');
onDropItem(item.item_id, item.id, 1);
setActiveDropdown(null);
}}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
{t('game.drop')} (x1)
</GameButton>
{item.quantity >= 5 && (
<GameButton
variant="danger"
size="sm"
onClick={() => {
playSfx('/audio/sfx/drop.wav');
onDropItem(item.item_id, item.id, 5);
setActiveDropdown(null);
}}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
{t('game.drop')} (x5)
</GameButton>
)}
{item.quantity > 1 && (
<GameButton
variant="danger"
size="sm"
onClick={() => {
playSfx('/audio/sfx/drop.wav');
onDropItem(item.item_id, item.id, item.quantity);
setActiveDropdown(null);
}}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
{t('game.drop')} ({t('common.all')})
</GameButton>
)}
</GameDropdown>
)}
</div>
);
};
return ( return (
<div className="workbench-overlay" onClick={(e: MouseEvent<HTMLDivElement>) => { <div className="workbench-overlay" onClick={(e: MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) handleClose() if (e.target === e.currentTarget) handleClose()
@@ -437,6 +775,17 @@ function InventoryModal({
value={inventoryFilter} value={inventoryFilter}
onChange={(e: ChangeEvent<HTMLInputElement>) => onSetInventoryFilter(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => onSetInventoryFilter(e.target.value)}
/> />
{/* View Mode Toggle */}
<div className="inventory-view-toggle">
<button
className={`view-toggle-btn ${viewMode === 'list' ? 'active' : ''}`}
onClick={toggleViewMode}
title={viewMode === 'list' ? "Switch to Grid View" : "Switch to List View"}
>
{viewMode === 'list' ? '📋' : '🔲'}
</button>
</div>
</div> </div>
<div className="inventory-items-grid"> <div className="inventory-items-grid">
@@ -452,14 +801,17 @@ function InventoryModal({
{filteredItems.some((item: any) => item.is_equipped) && ( {filteredItems.some((item: any) => item.is_equipped) && (
<> <>
<div className="category-header"> {t('game.equipped')}</div> <div className="category-header"> {t('game.equipped')}</div>
{filteredItems.filter((item: any) => item.is_equipped).map((item: any, i: number) => renderItemCard(item, i))} <div className={`items-container ${viewMode}`}>
{filteredItems.filter((item: any) => item.is_equipped).map((item: any, i: number) =>
viewMode === 'list' ? renderItemCard(item, i) : renderGridItem(item, i)
)}
</div>
</> </>
)} )}
{/* Backpack - grouped by categories */} {/* Backpack - grouped by categories */}
{filteredItems.some((item: any) => !item.is_equipped) && ( {filteredItems.some((item: any) => !item.is_equipped) && (
<> <>
<div className="category-header">🎒 {t('game.backpack')}</div>
{/* Group backpack items by category */} {/* Group backpack items by category */}
{categories {categories
.filter(cat => cat.id !== 'all') // Exclude 'all' from subcategories .filter(cat => cat.id !== 'all') // Exclude 'all' from subcategories
@@ -470,12 +822,16 @@ function InventoryModal({
if (categoryItems.length === 0) return null; if (categoryItems.length === 0) return null;
return ( return (
<div key={cat.id} className="backpack-category-section"> <div key={cat.id} className="backpack-category-section">
<div className="subcategory-header"> <div className="category-header">
<span className="subcat-icon">{cat.icon}</span> <span className="subcat-icon">{cat.icon}</span>
<span className="subcat-label">{cat.label}</span> <span className="subcat-label">{cat.label}</span>
<span className="subcat-count">({categoryItems.length})</span> <span className="subcat-count">({categoryItems.length})</span>
</div> </div>
{categoryItems.map((item: any, i: number) => renderItemCard(item, i))} <div className={`items-container ${viewMode}`}>
{categoryItems.map((item: any, i: number) =>
viewMode === 'list' ? renderItemCard(item, i) : renderGridItem(item, i)
)}
</div>
</div> </div>
); );
})} })}
@@ -484,7 +840,11 @@ function InventoryModal({
</> </>
) : ( ) : (
/* Single category */ /* Single category */
filteredItems.map((item: any, i: number) => renderItemCard(item, i)) <div className={`items-container ${viewMode}`}>
{filteredItems.map((item: any, i: number) =>
viewMode === 'list' ? renderItemCard(item, i) : renderGridItem(item, i)
)}
</div>
) )
)} )}
</div> </div>

View File

@@ -44,11 +44,12 @@ interface LocationViewProps {
onCraft: (itemId: number) => void onCraft: (itemId: number) => void
onRepair: (uniqueItemId: string, inventoryId: number) => void onRepair: (uniqueItemId: string, inventoryId: number) => void
onUncraft: (uniqueItemId: string, inventoryId: number) => void onUncraft: (uniqueItemId: string, inventoryId: number) => void
failedActionItemId: string | number | null
} }
function LocationView({ function LocationView({
location, location,
message,
locationMessages, locationMessages,
expandedCorpse, expandedCorpse,
corpseDetails, corpseDetails,
@@ -58,13 +59,14 @@ function LocationView({
workbenchTab, workbenchTab,
craftableItems, craftableItems,
repairableItems, repairableItems,
failedActionItemId,
uncraftableItems, uncraftableItems,
craftFilter, craftFilter,
repairFilter, repairFilter,
uncraftFilter, uncraftFilter,
craftCategoryFilter, craftCategoryFilter,
profile, profile,
onSetMessage,
onInitiateCombat, onInitiateCombat,
onInitiatePvP, onInitiatePvP,
onPickup, onPickup,
@@ -156,11 +158,11 @@ function LocationView({
</div> </div>
</div> </div>
{message && ( {/* {message && (
<div className="message-box" onClick={() => onSetMessage('')}> <div className="message-box" onClick={() => onSetMessage('')}>
{message} {message}
</div> </div>
)} )} */}
{locationMessages.length > 0 && ( {locationMessages.length > 0 && (
<div className="location-messages-log"> <div className="location-messages-log">
@@ -313,8 +315,10 @@ function LocationView({
<h3>{t('location.itemsOnGround')}</h3> <h3>{t('location.itemsOnGround')}</h3>
<div className="entity-list"> <div className="entity-list">
{location.items.map((item: any, i: number) => { {location.items.map((item: any, i: number) => {
// Use loose equality to handle potential string/number mismatches
const isShaking = failedActionItemId == item.id;
return ( return (
<div key={i} className="entity-card item-card"> <div key={i} className={`entity-card item-card ${isShaking ? 'shake' : ''}`}>
<GameTooltip content={ <GameTooltip content={
<div className="item-info-tooltip-content"> <div className="item-info-tooltip-content">
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>} {item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
@@ -377,7 +381,7 @@ function LocationView({
<button <button
className="action-btn pickup single" className="action-btn pickup single"
onClick={() => { onClick={() => {
playSfx('/audio/sfx/pickup.wav') // Sound handled in useGameEngine
onPickup(item.id, 1) onPickup(item.id, 1)
}} }}
> >
@@ -386,7 +390,6 @@ function LocationView({
) : ( ) : (
<div className="pickup-actions-group"> <div className="pickup-actions-group">
<button className="action-btn pickup" onClick={() => { <button className="action-btn pickup" onClick={() => {
playSfx('/audio/sfx/pickup.wav')
onPickup(item.id, 1) onPickup(item.id, 1)
}}> }}>
x1 x1
@@ -394,7 +397,6 @@ function LocationView({
{item.quantity >= 5 && ( {item.quantity >= 5 && (
<button className="action-btn pickup" onClick={() => { <button className="action-btn pickup" onClick={() => {
playSfx('/audio/sfx/pickup.wav')
onPickup(item.id, 5) onPickup(item.id, 5)
}}> }}>
x5 x5
@@ -403,7 +405,6 @@ function LocationView({
{item.quantity >= 10 && ( {item.quantity >= 10 && (
<button className="action-btn pickup" onClick={() => { <button className="action-btn pickup" onClick={() => {
playSfx('/audio/sfx/pickup.wav')
onPickup(item.id, 10) onPickup(item.id, 10)
}}> }}>
x10 x10
@@ -411,7 +412,6 @@ function LocationView({
)} )}
<button className="action-btn pickup" onClick={() => { <button className="action-btn pickup" onClick={() => {
playSfx('/audio/sfx/pickup.wav')
onPickup(item.id, item.quantity) onPickup(item.id, item.quantity)
}}> }}>
{t('common.all') || 'All'} {t('common.all') || 'All'}

View File

@@ -292,22 +292,6 @@ function PlayerSidebar({
<button <button
className="open-inventory-btn" className="open-inventory-btn"
onClick={() => setShowInventory(true)} onClick={() => setShowInventory(true)}
style={{
width: '100%',
padding: '0.5rem',
marginTop: '1rem',
backgroundColor: '#2c3e50',
border: '1px solid #34495e',
borderRadius: '8px',
color: '#ecf0f1',
fontSize: '1rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
transition: 'all 0.2s'
}}
> >
{t('game.inventory')} {t('game.inventory')}
</button> </button>

View File

@@ -201,7 +201,7 @@
padding: 0.75rem; padding: 0.75rem;
background: var(--game-bg-card); background: var(--game-bg-card);
border: 1px solid var(--game-border-color); border: 1px solid var(--game-border-color);
border-radius: var(--game-radius-md); clip-path: var(--game-clip-path);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
gap: 0.5rem; gap: 0.5rem;
@@ -331,15 +331,16 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: 6px;
border: 1px solid #4a5568; border: 1px solid #4a5568;
margin-right: 0.75rem; margin-right: 0.75rem;
clip-path: var(--game-clip-path-sm);
} }
.item-thumb-img { .item-thumb-img {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain; object-fit: contain;
clip-path: var(--game-clip-path-sm);
} }
.item-thumb-emoji { .item-thumb-emoji {
@@ -473,7 +474,7 @@
width: 120px; width: 120px;
height: 120px; height: 120px;
margin: 0 auto 1.5rem auto; margin: 0 auto 1.5rem auto;
border-radius: var(--game-radius-md); clip-path: var(--game-clip-path);
overflow: hidden; overflow: hidden;
border: 2px solid var(--game-border-color); border: 2px solid var(--game-border-color);
background: var(--game-bg-input); background: var(--game-bg-input);
@@ -504,7 +505,7 @@
max-width: 600px; max-width: 600px;
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px; clip-path: var(--game-clip-path);
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@@ -543,7 +544,7 @@
.uncraft-btn { .uncraft-btn {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
border-radius: 8px; clip-path: var(--game-clip-path);
border: none; border: none;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
@@ -609,7 +610,7 @@
.item-card-equipped { .item-card-equipped {
font-size: 0.7rem; font-size: 0.7rem;
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; clip-path: var(--game-clip-path-sm);
background: rgba(66, 153, 225, 0.2); background: rgba(66, 153, 225, 0.2);
color: #63b3ed; color: #63b3ed;
border: 1px solid rgba(66, 153, 225, 0.4); border: 1px solid rgba(66, 153, 225, 0.4);

View File

@@ -4,7 +4,7 @@
gap: 2px; gap: 2px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
padding: 2px; padding: 2px;
border-radius: 6px; clip-path: var(--game-clip-path-sm);
border: 1px solid rgba(72, 187, 120, 0.4); border: 1px solid rgba(72, 187, 120, 0.4);
/* Green border */ /* Green border */
align-items: center; align-items: center;
@@ -18,9 +18,9 @@
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
clip-path: var(--game-clip-path-sm);
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import api from '../../../services/api' import api from '../../../services/api'
import { useAudio } from '../../../contexts/AudioContext'
import type { import type {
PlayerState, PlayerState,
Location, Location,
@@ -36,6 +37,7 @@ export interface GameEngineState {
corpseDetails: any corpseDetails: any
movementCooldown: number movementCooldown: number
equipment: Equipment equipment: Equipment
failedActionItemId: string | number | null // For shake animation
// Workbench state // Workbench state
showCraftingMenu: boolean showCraftingMenu: boolean
@@ -145,6 +147,7 @@ export function useGameEngine(
_handleWebSocketMessage: (message: any) => Promise<void> _handleWebSocketMessage: (message: any) => Promise<void>
): [GameEngineState, GameEngineActions] { ): [GameEngineState, GameEngineActions] {
const { t } = useTranslation() const { t } = useTranslation()
const { playSfx } = useAudio()
// All state declarations // All state declarations
const [playerState, setPlayerState] = useState<PlayerState | null>(null) const [playerState, setPlayerState] = useState<PlayerState | null>(null)
const [location, setLocation] = useState<Location | null>(null) const [location, setLocation] = useState<Location | null>(null)
@@ -160,6 +163,7 @@ export function useGameEngine(
const [expandedCorpse, setExpandedCorpse] = useState<string | null>(null) const [expandedCorpse, setExpandedCorpse] = useState<string | null>(null)
const [corpseDetails, setCorpseDetails] = useState<any>(null) const [corpseDetails, setCorpseDetails] = useState<any>(null)
const [movementCooldown, setMovementCooldown] = useState<number>(0) const [movementCooldown, setMovementCooldown] = useState<number>(0)
const [failedActionItemId, setFailedActionItemId] = useState<string | number | null>(null)
// const [enemyTurnMessage, setEnemyTurnMessage] = useState<string>('') // Moved to Combat.tsx // const [enemyTurnMessage, setEnemyTurnMessage] = useState<string>('') // Moved to Combat.tsx
const [equipment, setEquipment] = useState<Equipment>({}) const [equipment, setEquipment] = useState<Equipment>({})
@@ -389,6 +393,7 @@ export function useGameEngine(
setMessage('Moving...') setMessage('Moving...')
const response = await api.post('/api/game/move', { direction }) const response = await api.post('/api/game/move', { direction })
setMessage(response.data.message) setMessage(response.data.message)
playSfx('/audio/sfx/step.wav')
setLocationMessages([]) setLocationMessages([])
if (response.data.encounter && response.data.encounter.triggered) { if (response.data.encounter && response.data.encounter.triggered) {
@@ -429,10 +434,16 @@ export function useGameEngine(
try { try {
const response = await api.post('/api/game/pickup', { item_id: itemId, quantity }) const response = await api.post('/api/game/pickup', { item_id: itemId, quantity })
addLocationMessage(response.data.message || 'Item picked up!') addLocationMessage(response.data.message || 'Item picked up!')
playSfx('/audio/sfx/pickup.wav')
setFailedActionItemId(null)
fetchGameData() fetchGameData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to pick up item') addLocationMessage(error.response?.data?.detail || 'Failed to pick up item')
fetchGameData() setFailedActionItemId(itemId)
// Reset shake after animation
setTimeout(() => setFailedActionItemId(null), 500)
// On error (e.g. full inventory), we don't strictly need to refresh game data immediately
// and it might interfere with the animation if it triggers a re-render.
} }
} }
@@ -515,6 +526,7 @@ export function useGameEngine(
corpseDetails, corpseDetails,
movementCooldown, movementCooldown,
equipment, equipment,
failedActionItemId,
showCraftingMenu, showCraftingMenu,
showRepairMenu, showRepairMenu,
craftableItems, craftableItems,
@@ -642,12 +654,12 @@ export function useGameEngine(
const handleCraft = async (itemId: string) => { const handleCraft = async (itemId: string) => {
try { try {
setMessage('Crafting...') // setMessage('Crafting...') // Loading state ok to keep specific or remove? Let's remove to avoid spam
const response = await api.post('/api/game/craft_item', { item_id: itemId }) const response = await api.post('/api/game/craft_item', { item_id: itemId })
setMessage(response.data.message || 'Item crafted!') addLocationMessage(response.data.message || 'Item crafted!')
await refreshWorkbenchData() await refreshWorkbenchData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to craft item') addLocationMessage(error.response?.data?.detail || 'Failed to craft item')
} }
} }
@@ -660,27 +672,27 @@ export function useGameEngine(
setWorkbenchTab('repair') setWorkbenchTab('repair')
setLoadedTabs(new Set(['repair'])) setLoadedTabs(new Set(['repair']))
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to load repair menu') addLocationMessage(error.response?.data?.detail || 'Failed to load repair menu')
} }
} }
const handleRepairFromMenu = async (uniqueItemId: number, inventoryId?: number) => { const handleRepairFromMenu = async (uniqueItemId: number, inventoryId?: number) => {
try { try {
setMessage('Repairing...') // setMessage('Repairing...')
const response = await api.post('/api/game/repair_item', { const response = await api.post('/api/game/repair_item', {
unique_item_id: uniqueItemId, unique_item_id: uniqueItemId,
inventory_id: inventoryId inventory_id: inventoryId
}) })
setMessage(response.data.message || 'Item repaired!') addLocationMessage(response.data.message || 'Item repaired!')
await refreshWorkbenchData() await refreshWorkbenchData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to repair item') addLocationMessage(error.response?.data?.detail || 'Failed to repair item')
} }
} }
const handleUncraft = async (uniqueItemId: number, inventoryId: number) => { const handleUncraft = async (uniqueItemId: number, inventoryId: number) => {
try { try {
setMessage('Salvaging...') // setMessage('Salvaging...')
const response = await api.post('/api/game/uncraft_item', { const response = await api.post('/api/game/uncraft_item', {
unique_item_id: uniqueItemId, unique_item_id: uniqueItemId,
inventory_id: inventoryId inventory_id: inventoryId
@@ -693,10 +705,10 @@ export function useGameEngine(
if (data.materials_lost && data.materials_lost.length > 0) { if (data.materials_lost && data.materials_lost.length > 0) {
msg += '\n⚠ Lost: ' + data.materials_lost.map((m: any) => `${m.emoji} ${m.name} x${m.quantity}`).join(', ') msg += '\n⚠ Lost: ' + data.materials_lost.map((m: any) => `${m.emoji} ${m.name} x${m.quantity}`).join(', ')
} }
setMessage(msg) addLocationMessage(msg)
await refreshWorkbenchData() await refreshWorkbenchData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to uncraft item') addLocationMessage(error.response?.data?.detail || 'Failed to uncraft item')
} }
} }
@@ -850,10 +862,10 @@ export function useGameEngine(
if (data.hp_change) { if (data.hp_change) {
msg += `\n❤ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}` msg += `\n❤ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}`
} }
setMessage(msg) addLocationMessage(msg)
fetchGameData() fetchGameData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Interaction failed') addLocationMessage(error.response?.data?.detail || 'Interaction failed')
} }
} }
@@ -875,7 +887,7 @@ export function useGameEngine(
item_index: itemIndex item_index: itemIndex
}) })
setMessage(response.data.message) addLocationMessage(response.data.message)
setTimeout(() => { }, 5000) setTimeout(() => { }, 5000)
if (response.data.corpse_empty) { if (response.data.corpse_empty) {
@@ -893,7 +905,7 @@ export function useGameEngine(
fetchGameData() fetchGameData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to loot corpse') addLocationMessage(error.response?.data?.detail || 'Failed to loot corpse')
} }
} }
@@ -903,12 +915,12 @@ export function useGameEngine(
const handleSpendPoint = async (stat: string) => { const handleSpendPoint = async (stat: string) => {
try { try {
setMessage(`Increasing ${stat}...`) // setMessage(`Increasing ${stat}...`)
const response = await api.post(`/api/game/spend_point?stat=${stat}`) const response = await api.post(`/api/game/spend_point?stat=${stat}`)
setMessage(response.data.message || 'Stat increased!') addLocationMessage(response.data.message || 'Stat increased!')
fetchGameData() fetchGameData()
} catch (error: any) { } catch (error: any) {
setMessage(error.response?.data?.detail || 'Failed to spend point') addLocationMessage(error.response?.data?.detail || 'Failed to spend point')
} }
} }

View File

@@ -23,6 +23,7 @@
--game-radius-md: 0px; --game-radius-md: 0px;
--game-clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px); --game-clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
--game-clip-path-sm: polygon(4px 0, 100% 0, 100% calc(100% - 4px), calc(100% - 4px) 100%, 0 100%, 0 4px); --game-clip-path-sm: polygon(4px 0, 100% 0, 100% calc(100% - 4px), calc(100% - 4px) 100%, 0 100%, 0 4px);
--game-clip-path-no-br: polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px);
/* --- Typography --- */ /* --- Typography --- */
--game-font-main: 'Saira Condensed', system-ui, sans-serif; --game-font-main: 'Saira Condensed', system-ui, sans-serif;
@@ -156,7 +157,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
aspect-ratio: 1; aspect-ratio: 1;
clip-path: var(--game-clip-path-sm); clip-path: var(--game-clip-path);
} }
.game-slot:hover { .game-slot:hover {