feat: Implement Inventory Grid View and GameButton
This commit is contained in:
@@ -10,6 +10,8 @@ hit.wav (When anyone takes damage)
|
||||
victory.wav (Combat won)
|
||||
defeat.wav (Combat lost)
|
||||
flee.wav (Successfully ran away)
|
||||
step.wav (Movement between locations)
|
||||
|
||||
Combat - Player Weapons
|
||||
The system detects keywords in the weapon name to pick the sound. If no match is found, it plays the default.
|
||||
|
||||
|
||||
BIN
pwa/public/audio/sfx/step.wav
Normal file
BIN
pwa/public/audio/sfx/step.wav
Normal file
Binary file not shown.
@@ -115,27 +115,6 @@ html {
|
||||
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 {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -457,7 +436,7 @@ html {
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: var(--game-radius-sm);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
@@ -516,11 +495,11 @@ html {
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: rgba(107, 185, 240, 0.15);
|
||||
border: 1px solid rgba(107, 185, 240, 0.4);
|
||||
border-radius: 16px;
|
||||
font-size: 0.85rem;
|
||||
color: #6bb9f0;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.location-tag:hover {
|
||||
@@ -708,8 +687,8 @@ html {
|
||||
margin: 1rem auto;
|
||||
aspect-ratio: 10 / 7;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 107, 107, 0.3);
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.location-image {
|
||||
@@ -721,19 +700,19 @@ html {
|
||||
.message-box {
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ff6b6b;
|
||||
color: #fff;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
/* Location Messages Log */
|
||||
.location-messages-log {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 107, 107, 0.2);
|
||||
background: var(--game-bg-panel);
|
||||
border: 1px solid var(--game-border-color);
|
||||
padding: 0.8rem;
|
||||
margin-top: 1rem;
|
||||
max-width: 100%;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.location-messages-log h4 {
|
||||
@@ -744,7 +723,8 @@ html {
|
||||
}
|
||||
|
||||
.messages-scroll {
|
||||
max-height: 150px;
|
||||
height: 5.5rem;
|
||||
/* Compact fixed height (~3 lines) */
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -756,7 +736,7 @@ html {
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -804,9 +784,9 @@ html {
|
||||
border: 1px solid var(--game-border-color);
|
||||
background: linear-gradient(135deg, rgba(80, 80, 90, 0.3) 0%, rgba(40, 40, 50, 0.5) 100%);
|
||||
color: var(--game-text-primary);
|
||||
border-radius: var(--game-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -879,9 +859,9 @@ html {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(255, 107, 107, 0.5);
|
||||
border: 1px solid rgba(255, 107, 107, 0.5);
|
||||
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.compass-icon {
|
||||
@@ -915,9 +895,9 @@ html {
|
||||
.compass-center-btn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
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%);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -985,10 +965,10 @@ html {
|
||||
margin: 0.5rem 0;
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
border: 2px solid rgba(255, 152, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
color: #ff9800;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
/* Special movement buttons */
|
||||
@@ -1005,9 +985,9 @@ html {
|
||||
background: rgba(107, 147, 255, 0.3);
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1034,10 +1014,10 @@ html {
|
||||
border: none;
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 0.95rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
@@ -1047,10 +1027,10 @@ html {
|
||||
|
||||
/* Interactables Section */
|
||||
.interactables-section {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: var(--game-bg-panel);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(255, 193, 7, 0.3);
|
||||
border: 1px solid var(--game-border-color);
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.interactables-section h3 {
|
||||
@@ -1065,13 +1045,13 @@ body.no-scroll {
|
||||
/* Interactable Card with Image */
|
||||
.interactable-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
border: 2px solid rgba(255, 193, 7, 0.4);
|
||||
border: 1px solid rgba(255, 193, 7, 0.4);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.interactable-card:hover {
|
||||
@@ -1136,10 +1116,10 @@ body.no-scroll {
|
||||
border: none;
|
||||
background: rgba(255, 193, 7, 0.3);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 0.9rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.interact-btn:hover {
|
||||
@@ -1160,10 +1140,10 @@ body.no-scroll {
|
||||
}
|
||||
|
||||
.entity-section {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: var(--game-bg-panel);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(255, 107, 107, 0.3);
|
||||
border: 1px solid var(--game-border-color);
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.entity-section h3 {
|
||||
@@ -1210,13 +1190,13 @@ body.no-scroll {
|
||||
.entity-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s;
|
||||
min-width: 320px;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.entity-card:hover {
|
||||
@@ -1306,12 +1286,12 @@ body.no-scroll {
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #6bb9f0, #89d4ff);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.entity-action-btn:hover {
|
||||
@@ -1375,9 +1355,9 @@ body.no-scroll {
|
||||
.corpse-details {
|
||||
background: rgba(156, 39, 176, 0.1);
|
||||
border: 2px solid rgba(156, 39, 176, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -1434,9 +1414,9 @@ body.no-scroll {
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.corpse-item:hover {
|
||||
@@ -1518,11 +1498,11 @@ body.no-scroll {
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #9c27b0, #ba68c8);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.loot-all-btn:hover {
|
||||
@@ -1535,9 +1515,9 @@ body.no-scroll {
|
||||
width: 100px;
|
||||
height: 70px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.entity-image img {
|
||||
@@ -1563,9 +1543,9 @@ body.no-scroll {
|
||||
.inventory-panel {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(107, 147, 255, 0.3);
|
||||
height: fit-content;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.inventory-panel h3 {
|
||||
@@ -1702,8 +1682,8 @@ body.no-scroll {
|
||||
.profile-section {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.profile-section h3 {
|
||||
@@ -1718,7 +1698,7 @@ body.no-scroll {
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 5px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.stat-row.highlight {
|
||||
@@ -1732,11 +1712,11 @@ body.no-scroll {
|
||||
border: none;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
@@ -1747,10 +1727,10 @@ body.no-scroll {
|
||||
|
||||
/* Profile Sidebar */
|
||||
.profile-sidebar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
background: var(--game-bg-panel);
|
||||
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 {
|
||||
@@ -1786,13 +1766,13 @@ body.no-scroll {
|
||||
.sidebar-progress-bar {
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 6px;
|
||||
overflow: visible;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.sidebar-progress-fill {
|
||||
@@ -1801,7 +1781,6 @@ body.no-scroll {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
@@ -1842,8 +1821,8 @@ body.no-scroll {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.capacity-info {
|
||||
@@ -1874,16 +1853,15 @@ body.no-scroll {
|
||||
.capacity-bar {
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 107, 107, 0.2);
|
||||
position: relative;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.capacity-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Single color for capacity bars */
|
||||
@@ -1918,17 +1896,17 @@ body.no-scroll {
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.stat-plus-btn {
|
||||
background: rgba(107, 185, 240, 0.3);
|
||||
border: 1px solid #6bb9f0;
|
||||
color: #6bb9f0;
|
||||
border-radius: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1974,11 +1952,11 @@ body.no-scroll {
|
||||
|
||||
/* Equipment Sidebar */
|
||||
.equipment-sidebar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
background: var(--game-bg-panel);
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
border: 1px solid var(--game-border-color);
|
||||
overflow: visible;
|
||||
clip-path: var(--game-clip-path);
|
||||
/* Allow tooltips to overflow */
|
||||
}
|
||||
|
||||
@@ -2029,12 +2007,12 @@ body.no-scroll {
|
||||
.equipment-slot {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
clip-path: var(--game-clip-path);
|
||||
/* Changed from center to space-between */
|
||||
gap: 0.25rem;
|
||||
/* Fixed dimensions for consistent sizing */
|
||||
@@ -2110,9 +2088,9 @@ body.no-scroll {
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
@@ -2141,8 +2119,8 @@ body.no-scroll {
|
||||
transform: translateX(-50%);
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
border: 2px solid #ff6b6b;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
min-width: 220px;
|
||||
max-width: 300px;
|
||||
z-index: 10000;
|
||||
@@ -2196,9 +2174,9 @@ body.no-scroll {
|
||||
font-size: 0.7rem;
|
||||
/* Slightly larger font */
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
@@ -2293,7 +2271,7 @@ body.no-scroll {
|
||||
color: #6bb9f0;
|
||||
background: rgba(107, 185, 240, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.equipment-slot-label {
|
||||
@@ -2305,10 +2283,10 @@ body.no-scroll {
|
||||
/* Inventory Sidebar */
|
||||
.inventory-sidebar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
padding-top: 3rem;
|
||||
border: 1px solid rgba(107, 185, 240, 0.3);
|
||||
clip-path: var(--game-clip-path);
|
||||
overflow: visible;
|
||||
/* Allow tooltips and dropdowns to show */
|
||||
position: relative;
|
||||
@@ -2336,10 +2314,10 @@ body.no-scroll {
|
||||
aspect-ratio: 1;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -2387,7 +2365,7 @@ body.no-scroll {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.sidebar-empty {
|
||||
@@ -2465,9 +2443,9 @@ body.no-scroll {
|
||||
.inventory-item-row-hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -2501,9 +2479,9 @@ body.no-scroll {
|
||||
left: 0;
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
border: 2px solid #6bb9f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
min-width: 220px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
max-width: 300px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
@@ -2540,10 +2518,10 @@ body.no-scroll {
|
||||
border: 1px solid #6bb9f0;
|
||||
color: #6bb9f0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
@@ -2606,9 +2584,9 @@ body.no-scroll {
|
||||
right: 0;
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
border: 2px solid #6bb9f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
min-width: 220px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
max-width: 300px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
@@ -2710,9 +2688,9 @@ body.no-scroll {
|
||||
padding-bottom: 4px;
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
border: 2px solid #f44336;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
min-width: 120px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
flex-direction: column;
|
||||
@@ -2741,10 +2719,10 @@ body.no-scroll {
|
||||
border: 1px solid #f44336;
|
||||
color: #f44336;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -2769,9 +2747,9 @@ body.no-scroll {
|
||||
padding-bottom: 4px;
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
border: 2px solid #4caf50;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
min-width: 140px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
flex-direction: column;
|
||||
@@ -2799,10 +2777,10 @@ body.no-scroll {
|
||||
border: 1px solid #4caf50;
|
||||
color: #4caf50;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -2823,10 +2801,10 @@ body.no-scroll {
|
||||
.inventory-item-row {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -2854,8 +2832,8 @@ body.no-scroll {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.item-icon-small img {
|
||||
@@ -2888,7 +2866,7 @@ body.no-scroll {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
/* Category Filter */
|
||||
@@ -2903,10 +2881,10 @@ body.no-scroll {
|
||||
padding: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -2956,9 +2934,9 @@ body.no-scroll {
|
||||
.item-actions-panel {
|
||||
background: rgba(107, 185, 240, 0.1);
|
||||
border: 2px solid #6bb9f0;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.item-details-header {
|
||||
@@ -3010,11 +2988,11 @@ body.no-scroll {
|
||||
.item-action-btn {
|
||||
padding: 0.6rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.item-action-btn.use-btn {
|
||||
@@ -3110,9 +3088,9 @@ body.no-scroll {
|
||||
width: 80px;
|
||||
height: 56px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.entity-image img {
|
||||
@@ -3140,12 +3118,12 @@ body.no-scroll {
|
||||
.combat-modal {
|
||||
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
||||
border: 2px solid rgba(107, 185, 240, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
animation: slideUp 0.3s;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.combat-header {
|
||||
@@ -3169,16 +3147,16 @@ body.no-scroll {
|
||||
padding: 1.5rem;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 2px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 12px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-enemy-image-container {
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-enemy-image {
|
||||
@@ -3211,9 +3189,9 @@ body.no-scroll {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(244, 67, 54, 0.4);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-hp-fill {
|
||||
@@ -3228,10 +3206,10 @@ body.no-scroll {
|
||||
padding: 1rem;
|
||||
background: rgba(107, 185, 240, 0.1);
|
||||
border-left: 4px solid #6bb9f0;
|
||||
border-radius: 8px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-log p {
|
||||
@@ -3264,11 +3242,11 @@ body.no-scroll {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-action-btn:disabled {
|
||||
@@ -3340,8 +3318,8 @@ body.no-scroll {
|
||||
padding: 2rem;
|
||||
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-radius: 12px;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.combat-header-inline {
|
||||
@@ -3367,10 +3345,10 @@ body.no-scroll {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
aspect-ratio: 10 / 7;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 3px solid rgba(244, 67, 54, 0.5);
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.combat-enemy-image-large img {
|
||||
@@ -3427,11 +3405,11 @@ body.no-scroll {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(255, 100, 100, 0.6);
|
||||
position: relative;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-hp-fill-inline {
|
||||
@@ -3454,13 +3432,13 @@ body.no-scroll {
|
||||
.combat-log-inline {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-log-inline p {
|
||||
@@ -3475,13 +3453,13 @@ body.no-scroll {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid transparent;
|
||||
min-height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
/* Apply pulsing animation when it's enemy's turn processing */
|
||||
@@ -3533,11 +3511,11 @@ body.no-scroll {
|
||||
margin-top: 2rem;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-log-container h4 {
|
||||
@@ -3563,13 +3541,13 @@ body.no-scroll {
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left: 3px solid rgba(244, 67, 54, 0.5);
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
animation: fadeInLog 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.combat-log-entry.player-action {
|
||||
@@ -3671,7 +3649,6 @@ body.no-scroll {
|
||||
.location-description-box {
|
||||
background: rgba(25, 26, 31, 0.6);
|
||||
border: 1px solid rgba(107, 185, 240, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
@@ -3679,6 +3656,7 @@ body.no-scroll {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-sizing: border-box;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.location-description {
|
||||
@@ -3751,13 +3729,13 @@ body.no-scroll {
|
||||
background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 68, 68, 0.5);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.pvp-btn:hover {
|
||||
@@ -3795,14 +3773,31 @@ body.no-scroll {
|
||||
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 {
|
||||
background: rgba(30, 30, 40, 0.8);
|
||||
border: 2px solid rgba(255, 107, 107, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
min-width: 280px;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.pvp-player-card.your-card {
|
||||
@@ -4318,8 +4313,7 @@ body.no-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 220px;
|
||||
/* Reserve space for grid to prevent layout shift */
|
||||
/* min-height removed to fit content */
|
||||
justify-content: center;
|
||||
/* Center content vertically */
|
||||
}
|
||||
@@ -4330,7 +4324,7 @@ body.no-scroll {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.combat-actions-group .btn {
|
||||
.combat-actions .btn {
|
||||
padding: 1rem;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
@@ -4398,7 +4392,7 @@ body.no-scroll {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
overflow: visible;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
@@ -4505,7 +4499,7 @@ body.no-scroll {
|
||||
gap: 2px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||
/* Green border */
|
||||
align-items: center;
|
||||
@@ -4519,7 +4513,6 @@ body.no-scroll {
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
@@ -4540,3 +4533,32 @@ body.no-scroll {
|
||||
.action-btn.pickup.single:hover {
|
||||
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;
|
||||
}
|
||||
@@ -457,6 +457,7 @@ function Game() {
|
||||
onCraft={async (itemId: number) => await actions.handleCraft(itemId.toString())}
|
||||
onRepair={(uniqueItemId: string, inventoryId: number) => actions.handleRepairFromMenu(Number(uniqueItemId), inventoryId)}
|
||||
onUncraft={(uniqueItemId: string, inventoryId: number) => actions.handleUncraft(Number(uniqueItemId), inventoryId)}
|
||||
failedActionItemId={state.failedActionItemId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
background: linear-gradient(135deg, rgba(225, 29, 72, 0.1) 0%, transparent 100%);
|
||||
border: 1px solid rgba(225, 29, 72, 0.3);
|
||||
/* 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);
|
||||
position: relative;
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
padding: 0 12px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
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;
|
||||
color: #aaddaa;
|
||||
font-family: monospace;
|
||||
@@ -177,7 +177,7 @@
|
||||
font-weight: 700;
|
||||
transition: all 0.2s;
|
||||
/* 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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -209,7 +209,7 @@
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
/* 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 {
|
||||
@@ -223,7 +223,7 @@
|
||||
.game-header .language-btn {
|
||||
height: 36px;
|
||||
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);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
@@ -237,7 +237,7 @@
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--game-border-color);
|
||||
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 */
|
||||
|
||||
117
pwa/src/components/common/GameButton.css
Normal file
117
pwa/src/components/common/GameButton.css
Normal 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);
|
||||
}
|
||||
38
pwa/src/components/common/GameButton.tsx
Normal file
38
pwa/src/components/common/GameButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
97
pwa/src/components/common/GameDropdown.css
Normal file
97
pwa/src/components/common/GameDropdown.css
Normal 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;
|
||||
}
|
||||
99
pwa/src/components/common/GameDropdown.tsx
Normal file
99
pwa/src/components/common/GameDropdown.tsx
Normal 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
|
||||
);
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export const GameTooltip: React.FC<GameTooltipProps> = ({ content, children, cla
|
||||
<div style={{
|
||||
background: 'var(--game-bg-tooltip, #151515)',
|
||||
border: '1px solid var(--game-border-color, #333)',
|
||||
borderRadius: 'var(--game-radius-sm, 4px)',
|
||||
clipPath: 'var(--game-clip-path)',
|
||||
padding: '0.5rem 0.8rem',
|
||||
boxShadow: 'var(--game-shadow-tooltip, 0 4px 12px rgba(0,0,0,0.5))',
|
||||
color: 'var(--game-text-primary, #ddd)',
|
||||
|
||||
@@ -248,18 +248,18 @@ export const Combat: React.FC<CombatProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle combat_over from WebSocket
|
||||
if (initialCombatData?.combat_over) {
|
||||
const pvp = initialCombatData?.pvp_combat;
|
||||
const myId = pvp?.is_attacker
|
||||
? pvp?.attacker?.id
|
||||
: pvp?.defender?.id;
|
||||
// Handle combat_over from WebSocket or initial state (including fled states which might not set combat_over flag explicitly in all paths)
|
||||
const pvp = initialCombatData?.pvp_combat;
|
||||
const myId = pvp?.is_attacker
|
||||
? pvp?.attacker?.id
|
||||
: pvp?.defender?.id;
|
||||
|
||||
// Check if someone fled
|
||||
const iAmAttacker = pvp?.is_attacker;
|
||||
const opponentFled = iAmAttacker ? pvp?.defender_fled : pvp?.attacker_fled;
|
||||
const iFled = iAmAttacker ? pvp?.attacker_fled : pvp?.defender_fled;
|
||||
// Check if someone fled (Robust check for both combat_over=true AND implicit state)
|
||||
const iAmAttacker = pvp?.is_attacker;
|
||||
const opponentFled = iAmAttacker ? pvp?.defender_fled : pvp?.attacker_fled;
|
||||
const iFled = iAmAttacker ? pvp?.attacker_fled : pvp?.defender_fled;
|
||||
|
||||
if (initialCombatData?.combat_over || opponentFled || iFled) {
|
||||
if (opponentFled) {
|
||||
// Opponent fled - I "win" by default
|
||||
setCombatResult('victory');
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
/* More transparent/themed background */
|
||||
background: rgba(20, 20, 20, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -118,10 +118,10 @@
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.health-bar-fill {
|
||||
@@ -197,12 +197,12 @@
|
||||
/* Combat Log */
|
||||
.combat-log-container {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.9rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.log-message {
|
||||
@@ -236,7 +236,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.floating-text {
|
||||
@@ -246,6 +246,8 @@
|
||||
animation: float-up 5s forwards;
|
||||
pointer-events: none;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
z-index: 2000;
|
||||
/* Ensure on top of everything */
|
||||
}
|
||||
|
||||
.type-damage {
|
||||
@@ -346,10 +348,10 @@
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 20px;
|
||||
font-size: 1.5rem;
|
||||
animation: pulse 1s infinite;
|
||||
z-index: 20;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -419,10 +421,10 @@
|
||||
.stat-block {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.stat-block.enemy {
|
||||
@@ -463,13 +465,12 @@
|
||||
height: 12px;
|
||||
/* Slightly thinner than header */
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease-out;
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
margin-bottom: 1.5rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #3a4b5c;
|
||||
border-radius: 8px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
@@ -118,7 +118,7 @@
|
||||
flex-direction: row;
|
||||
background-color: rgba(26, 32, 44, 0.8);
|
||||
border: 1px solid #2d3748;
|
||||
border-radius: 0.5rem;
|
||||
clip-path: var(--game-clip-path);
|
||||
padding: 0.75rem;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
@@ -142,7 +142,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path);
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
|
||||
.stat-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -305,7 +305,7 @@
|
||||
color: #48bb78;
|
||||
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
@@ -156,17 +156,46 @@ export const CombatView: React.FC<CombatViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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) */}
|
||||
<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
|
||||
label={state.npcName || t('common.enemy')}
|
||||
value={state.npcHp}
|
||||
@@ -180,13 +209,6 @@ export const CombatView: React.FC<CombatViewProps> = ({
|
||||
|
||||
{/* Player HP (Right) */}
|
||||
<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
|
||||
label={playerName || t('common.you')}
|
||||
value={state.playerHp}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
|
||||
.metric-fill {
|
||||
height: 100%;
|
||||
border-radius: var(--game-radius-sm);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -191,7 +191,8 @@
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -231,12 +232,12 @@
|
||||
background: var(--game-bg-app);
|
||||
}
|
||||
|
||||
.inventory-search-bar {
|
||||
.game-search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
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);
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--game-text-primary);
|
||||
@@ -245,7 +246,12 @@
|
||||
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;
|
||||
border: none;
|
||||
color: #fff;
|
||||
@@ -254,6 +260,41 @@
|
||||
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 {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -263,6 +304,30 @@
|
||||
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 */
|
||||
.inventory-item-card.compact {
|
||||
@@ -270,7 +335,7 @@
|
||||
flex-direction: row;
|
||||
background-color: var(--game-bg-card);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
clip-path: var(--game-clip-path);
|
||||
padding: 0.75rem;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
@@ -285,6 +350,45 @@
|
||||
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 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
@@ -294,7 +398,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path-no-br);
|
||||
border: 1px solid #4a5568;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -312,6 +416,11 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Grid Icon adjustments */
|
||||
.inventory-item-card.grid .item-icon-large {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.item-icon-large.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -325,11 +434,32 @@
|
||||
color: var(--game-text-primary);
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--game-radius-sm);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-weight: bold;
|
||||
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 {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -374,7 +504,7 @@
|
||||
.item-tier-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
background: #4a5568;
|
||||
color: #e2e8f0;
|
||||
font-weight: bold;
|
||||
@@ -510,7 +640,7 @@
|
||||
|
||||
.stat-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -611,7 +741,8 @@
|
||||
.durability-track {
|
||||
height: 0.5rem;
|
||||
background-color: #374151;
|
||||
border-radius: 9999px;
|
||||
background-color: #374151;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -661,6 +792,9 @@
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid #4a5568;
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-actions-section.bottom-right {
|
||||
@@ -671,13 +805,17 @@
|
||||
|
||||
.action-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--game-border-color);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn:disabled,
|
||||
@@ -730,7 +868,7 @@
|
||||
gap: 2px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
border: 1px solid rgba(245, 101, 101, 0.3);
|
||||
}
|
||||
|
||||
@@ -740,7 +878,7 @@
|
||||
border: none;
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.action-btn.drop:hover {
|
||||
@@ -772,7 +910,7 @@
|
||||
.item-card-equipped {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
background: rgba(66, 153, 225, 0.2);
|
||||
color: #63b3ed;
|
||||
border: 1px solid rgba(66, 153, 225, 0.4);
|
||||
@@ -793,31 +931,41 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subcategory-header {
|
||||
/* Tooltip Custom Content */
|
||||
.item-tooltip-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-left: 3px solid #4299e1;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0 4px 4px 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.subcat-icon {
|
||||
font-size: 1rem;
|
||||
.tooltip-header {
|
||||
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 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
.tooltip-desc {
|
||||
color: #a0aec0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.subcat-count {
|
||||
font-size: 0.75rem;
|
||||
color: #718096;
|
||||
margin-left: auto;
|
||||
.tooltip-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MouseEvent, ChangeEvent, useEffect } from 'react'
|
||||
import { MouseEvent, ChangeEvent, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAudio } from '../../contexts/AudioContext'
|
||||
import { PlayerState, Profile, Equipment } from './types'
|
||||
@@ -7,6 +7,9 @@ import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
import './InventoryModal.css'
|
||||
import { EffectBadge } from './EffectBadge'
|
||||
import { GameTooltip } from '../common/GameTooltip'
|
||||
import { GameDropdown } from '../common/GameDropdown'
|
||||
import { GameButton } from '../common/GameButton'
|
||||
import '../common/GameDropdown.css'
|
||||
|
||||
interface InventoryModalProps {
|
||||
playerState: PlayerState
|
||||
@@ -40,6 +43,15 @@ function InventoryModal({
|
||||
const { t } = useTranslation()
|
||||
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
|
||||
useEffect(() => {
|
||||
playSfx('/audio/sfx/inventory_open.wav')
|
||||
@@ -49,6 +61,26 @@ function InventoryModal({
|
||||
// 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 = () => {
|
||||
playSfx('/audio/sfx/inventory_close.wav')
|
||||
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 (
|
||||
<div className="workbench-overlay" onClick={(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) handleClose()
|
||||
@@ -437,6 +775,17 @@ function InventoryModal({
|
||||
value={inventoryFilter}
|
||||
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 className="inventory-items-grid">
|
||||
@@ -452,14 +801,17 @@ function InventoryModal({
|
||||
{filteredItems.some((item: any) => item.is_equipped) && (
|
||||
<>
|
||||
<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 */}
|
||||
{filteredItems.some((item: any) => !item.is_equipped) && (
|
||||
<>
|
||||
<div className="category-header">🎒 {t('game.backpack')}</div>
|
||||
{/* Group backpack items by category */}
|
||||
{categories
|
||||
.filter(cat => cat.id !== 'all') // Exclude 'all' from subcategories
|
||||
@@ -470,12 +822,16 @@ function InventoryModal({
|
||||
if (categoryItems.length === 0) return null;
|
||||
return (
|
||||
<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-label">{cat.label}</span>
|
||||
<span className="subcat-count">({categoryItems.length})</span>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
@@ -484,7 +840,11 @@ function InventoryModal({
|
||||
</>
|
||||
) : (
|
||||
/* 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>
|
||||
|
||||
@@ -44,11 +44,12 @@ interface LocationViewProps {
|
||||
onCraft: (itemId: number) => void
|
||||
onRepair: (uniqueItemId: string, inventoryId: number) => void
|
||||
onUncraft: (uniqueItemId: string, inventoryId: number) => void
|
||||
failedActionItemId: string | number | null
|
||||
}
|
||||
|
||||
function LocationView({
|
||||
location,
|
||||
message,
|
||||
|
||||
locationMessages,
|
||||
expandedCorpse,
|
||||
corpseDetails,
|
||||
@@ -58,13 +59,14 @@ function LocationView({
|
||||
workbenchTab,
|
||||
craftableItems,
|
||||
repairableItems,
|
||||
failedActionItemId,
|
||||
uncraftableItems,
|
||||
craftFilter,
|
||||
repairFilter,
|
||||
uncraftFilter,
|
||||
craftCategoryFilter,
|
||||
profile,
|
||||
onSetMessage,
|
||||
|
||||
onInitiateCombat,
|
||||
onInitiatePvP,
|
||||
onPickup,
|
||||
@@ -156,11 +158,11 @@ function LocationView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
{/* {message && (
|
||||
<div className="message-box" onClick={() => onSetMessage('')}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{locationMessages.length > 0 && (
|
||||
<div className="location-messages-log">
|
||||
@@ -313,8 +315,10 @@ function LocationView({
|
||||
<h3>{t('location.itemsOnGround')}</h3>
|
||||
<div className="entity-list">
|
||||
{location.items.map((item: any, i: number) => {
|
||||
// Use loose equality to handle potential string/number mismatches
|
||||
const isShaking = failedActionItemId == item.id;
|
||||
return (
|
||||
<div key={i} className="entity-card item-card">
|
||||
<div key={i} className={`entity-card item-card ${isShaking ? 'shake' : ''}`}>
|
||||
<GameTooltip content={
|
||||
<div className="item-info-tooltip-content">
|
||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||
@@ -377,7 +381,7 @@ function LocationView({
|
||||
<button
|
||||
className="action-btn pickup single"
|
||||
onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
// Sound handled in useGameEngine
|
||||
onPickup(item.id, 1)
|
||||
}}
|
||||
>
|
||||
@@ -386,7 +390,6 @@ function LocationView({
|
||||
) : (
|
||||
<div className="pickup-actions-group">
|
||||
<button className="action-btn pickup" onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
onPickup(item.id, 1)
|
||||
}}>
|
||||
x1
|
||||
@@ -394,7 +397,6 @@ function LocationView({
|
||||
|
||||
{item.quantity >= 5 && (
|
||||
<button className="action-btn pickup" onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
onPickup(item.id, 5)
|
||||
}}>
|
||||
x5
|
||||
@@ -403,7 +405,6 @@ function LocationView({
|
||||
|
||||
{item.quantity >= 10 && (
|
||||
<button className="action-btn pickup" onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
onPickup(item.id, 10)
|
||||
}}>
|
||||
x10
|
||||
@@ -411,7 +412,6 @@ function LocationView({
|
||||
)}
|
||||
|
||||
<button className="action-btn pickup" onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
onPickup(item.id, item.quantity)
|
||||
}}>
|
||||
{t('common.all') || 'All'}
|
||||
|
||||
@@ -292,22 +292,6 @@ function PlayerSidebar({
|
||||
<button
|
||||
className="open-inventory-btn"
|
||||
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')}
|
||||
</button>
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
padding: 0.75rem;
|
||||
background: var(--game-bg-card);
|
||||
border: 1px solid var(--game-border-color);
|
||||
border-radius: var(--game-radius-md);
|
||||
clip-path: var(--game-clip-path);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
gap: 0.5rem;
|
||||
@@ -331,15 +331,16 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 6px;
|
||||
border: 1px solid #4a5568;
|
||||
margin-right: 0.75rem;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.item-thumb-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
}
|
||||
|
||||
.item-thumb-emoji {
|
||||
@@ -473,7 +474,7 @@
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 1.5rem auto;
|
||||
border-radius: var(--game-radius-md);
|
||||
clip-path: var(--game-clip-path);
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--game-border-color);
|
||||
background: var(--game-bg-input);
|
||||
@@ -504,7 +505,7 @@
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
clip-path: var(--game-clip-path);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@@ -543,7 +544,7 @@
|
||||
.uncraft-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
clip-path: var(--game-clip-path);
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
@@ -609,7 +610,7 @@
|
||||
.item-card-equipped {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
background: rgba(66, 153, 225, 0.2);
|
||||
color: #63b3ed;
|
||||
border: 1px solid rgba(66, 153, 225, 0.4);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
gap: 2px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||
/* Green border */
|
||||
align-items: center;
|
||||
@@ -18,9 +18,9 @@
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../../../services/api'
|
||||
import { useAudio } from '../../../contexts/AudioContext'
|
||||
import type {
|
||||
PlayerState,
|
||||
Location,
|
||||
@@ -36,6 +37,7 @@ export interface GameEngineState {
|
||||
corpseDetails: any
|
||||
movementCooldown: number
|
||||
equipment: Equipment
|
||||
failedActionItemId: string | number | null // For shake animation
|
||||
|
||||
// Workbench state
|
||||
showCraftingMenu: boolean
|
||||
@@ -145,6 +147,7 @@ export function useGameEngine(
|
||||
_handleWebSocketMessage: (message: any) => Promise<void>
|
||||
): [GameEngineState, GameEngineActions] {
|
||||
const { t } = useTranslation()
|
||||
const { playSfx } = useAudio()
|
||||
// All state declarations
|
||||
const [playerState, setPlayerState] = useState<PlayerState | null>(null)
|
||||
const [location, setLocation] = useState<Location | null>(null)
|
||||
@@ -160,6 +163,7 @@ export function useGameEngine(
|
||||
const [expandedCorpse, setExpandedCorpse] = useState<string | null>(null)
|
||||
const [corpseDetails, setCorpseDetails] = useState<any>(null)
|
||||
const [movementCooldown, setMovementCooldown] = useState<number>(0)
|
||||
const [failedActionItemId, setFailedActionItemId] = useState<string | number | null>(null)
|
||||
// const [enemyTurnMessage, setEnemyTurnMessage] = useState<string>('') // Moved to Combat.tsx
|
||||
|
||||
const [equipment, setEquipment] = useState<Equipment>({})
|
||||
@@ -389,6 +393,7 @@ export function useGameEngine(
|
||||
setMessage('Moving...')
|
||||
const response = await api.post('/api/game/move', { direction })
|
||||
setMessage(response.data.message)
|
||||
playSfx('/audio/sfx/step.wav')
|
||||
setLocationMessages([])
|
||||
|
||||
if (response.data.encounter && response.data.encounter.triggered) {
|
||||
@@ -429,10 +434,16 @@ export function useGameEngine(
|
||||
try {
|
||||
const response = await api.post('/api/game/pickup', { item_id: itemId, quantity })
|
||||
addLocationMessage(response.data.message || 'Item picked up!')
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
setFailedActionItemId(null)
|
||||
fetchGameData()
|
||||
} catch (error: any) {
|
||||
setMessage(error.response?.data?.detail || 'Failed to pick up item')
|
||||
fetchGameData()
|
||||
addLocationMessage(error.response?.data?.detail || 'Failed to pick up item')
|
||||
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,
|
||||
movementCooldown,
|
||||
equipment,
|
||||
failedActionItemId,
|
||||
showCraftingMenu,
|
||||
showRepairMenu,
|
||||
craftableItems,
|
||||
@@ -642,12 +654,12 @@ export function useGameEngine(
|
||||
|
||||
const handleCraft = async (itemId: string) => {
|
||||
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 })
|
||||
setMessage(response.data.message || 'Item crafted!')
|
||||
addLocationMessage(response.data.message || 'Item crafted!')
|
||||
await refreshWorkbenchData()
|
||||
} 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')
|
||||
setLoadedTabs(new Set(['repair']))
|
||||
} 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) => {
|
||||
try {
|
||||
setMessage('Repairing...')
|
||||
// setMessage('Repairing...')
|
||||
const response = await api.post('/api/game/repair_item', {
|
||||
unique_item_id: uniqueItemId,
|
||||
inventory_id: inventoryId
|
||||
})
|
||||
setMessage(response.data.message || 'Item repaired!')
|
||||
addLocationMessage(response.data.message || 'Item repaired!')
|
||||
await refreshWorkbenchData()
|
||||
} 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) => {
|
||||
try {
|
||||
setMessage('Salvaging...')
|
||||
// setMessage('Salvaging...')
|
||||
const response = await api.post('/api/game/uncraft_item', {
|
||||
unique_item_id: uniqueItemId,
|
||||
inventory_id: inventoryId
|
||||
@@ -693,10 +705,10 @@ export function useGameEngine(
|
||||
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(', ')
|
||||
}
|
||||
setMessage(msg)
|
||||
addLocationMessage(msg)
|
||||
await refreshWorkbenchData()
|
||||
} 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) {
|
||||
msg += `\n❤️ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}`
|
||||
}
|
||||
setMessage(msg)
|
||||
addLocationMessage(msg)
|
||||
fetchGameData()
|
||||
} 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
|
||||
})
|
||||
|
||||
setMessage(response.data.message)
|
||||
addLocationMessage(response.data.message)
|
||||
setTimeout(() => { }, 5000)
|
||||
|
||||
if (response.data.corpse_empty) {
|
||||
@@ -893,7 +905,7 @@ export function useGameEngine(
|
||||
|
||||
fetchGameData()
|
||||
} 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) => {
|
||||
try {
|
||||
setMessage(`Increasing ${stat}...`)
|
||||
// setMessage(`Increasing ${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()
|
||||
} catch (error: any) {
|
||||
setMessage(error.response?.data?.detail || 'Failed to spend point')
|
||||
addLocationMessage(error.response?.data?.detail || 'Failed to spend point')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
--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-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 --- */
|
||||
--game-font-main: 'Saira Condensed', system-ui, sans-serif;
|
||||
@@ -156,7 +157,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
clip-path: var(--game-clip-path-sm);
|
||||
clip-path: var(--game-clip-path);
|
||||
}
|
||||
|
||||
.game-slot:hover {
|
||||
|
||||
Reference in New Issue
Block a user