feat: Implement Inventory Grid View and GameButton

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

View File

@@ -10,6 +10,8 @@ hit.wav (When anyone takes damage)
victory.wav (Combat won)
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.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ export const GameTooltip: React.FC<GameTooltipProps> = ({ content, children, cla
<div style={{
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)',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { MouseEvent, ChangeEvent, useEffect } from 'react'
import { MouseEvent, ChangeEvent, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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