diff --git a/pwa/public/audio/audios.txt b/pwa/public/audio/audios.txt index 962ea36..e1416a5 100644 --- a/pwa/public/audio/audios.txt +++ b/pwa/public/audio/audios.txt @@ -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. diff --git a/pwa/public/audio/sfx/step.wav b/pwa/public/audio/sfx/step.wav new file mode 100644 index 0000000..4f81ee4 Binary files /dev/null and b/pwa/public/audio/sfx/step.wav differ diff --git a/pwa/src/components/Game.css b/pwa/src/components/Game.css index 6f5cfe4..41a1746 100644 --- a/pwa/src/components/Game.css +++ b/pwa/src/components/Game.css @@ -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; @@ -4539,4 +4532,33 @@ 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; } \ No newline at end of file diff --git a/pwa/src/components/Game.tsx b/pwa/src/components/Game.tsx index ce90335..2fa03ff 100644 --- a/pwa/src/components/Game.tsx +++ b/pwa/src/components/Game.tsx @@ -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} /> )} diff --git a/pwa/src/components/GameHeader.css b/pwa/src/components/GameHeader.css index 19c3d67..6bfdbbf 100644 --- a/pwa/src/components/GameHeader.css +++ b/pwa/src/components/GameHeader.css @@ -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 */ diff --git a/pwa/src/components/common/GameButton.css b/pwa/src/components/common/GameButton.css new file mode 100644 index 0000000..6ddff7e --- /dev/null +++ b/pwa/src/components/common/GameButton.css @@ -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); +} \ No newline at end of file diff --git a/pwa/src/components/common/GameButton.tsx b/pwa/src/components/common/GameButton.tsx new file mode 100644 index 0000000..70a06dd --- /dev/null +++ b/pwa/src/components/common/GameButton.tsx @@ -0,0 +1,38 @@ +import React, { ReactNode } from 'react'; +import './GameButton.css'; + +interface GameButtonProps { + children: ReactNode; + onClick?: (e: React.MouseEvent) => void; + variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'info' | 'warning'; + size?: 'sm' | 'md' | 'lg'; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; +} + +export const GameButton: React.FC = ({ + children, + onClick, + variant = 'primary', + size = 'md', + disabled = false, + className = '', + style +}) => { + const handleClick = (e: React.MouseEvent) => { + if (disabled) return; + if (onClick) onClick(e); + }; + + return ( + + ); +}; diff --git a/pwa/src/components/common/GameDropdown.css b/pwa/src/components/common/GameDropdown.css new file mode 100644 index 0000000..8fc09b8 --- /dev/null +++ b/pwa/src/components/common/GameDropdown.css @@ -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; +} \ No newline at end of file diff --git a/pwa/src/components/common/GameDropdown.tsx b/pwa/src/components/common/GameDropdown.tsx new file mode 100644 index 0000000..a80a20e --- /dev/null +++ b/pwa/src/components/common/GameDropdown.tsx @@ -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 = ({ + isOpen, + onClose, + position, + children, + className = '', + width = '200px' +}) => { + const dropdownRef = useRef(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( +
e.stopPropagation()} // Prevent clicks inside from closing + > + {children} +
, + document.body + ); +}; diff --git a/pwa/src/components/common/GameTooltip.tsx b/pwa/src/components/common/GameTooltip.tsx index 08fae6d..e11e1c5 100644 --- a/pwa/src/components/common/GameTooltip.tsx +++ b/pwa/src/components/common/GameTooltip.tsx @@ -73,7 +73,7 @@ export const GameTooltip: React.FC = ({ content, children, cla
= ({ } } - // 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'); diff --git a/pwa/src/components/game/CombatEffects.css b/pwa/src/components/game/CombatEffects.css index 2c2c26d..595694f 100644 --- a/pwa/src/components/game/CombatEffects.css +++ b/pwa/src/components/game/CombatEffects.css @@ -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; } \ No newline at end of file diff --git a/pwa/src/components/game/CombatInventoryModal.css b/pwa/src/components/game/CombatInventoryModal.css index 95e3444..274b38c 100644 --- a/pwa/src/components/game/CombatInventoryModal.css +++ b/pwa/src/components/game/CombatInventoryModal.css @@ -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; diff --git a/pwa/src/components/game/CombatView.tsx b/pwa/src/components/game/CombatView.tsx index f218206..1e5c935 100644 --- a/pwa/src/components/game/CombatView.tsx +++ b/pwa/src/components/game/CombatView.tsx @@ -156,17 +156,46 @@ export const CombatView: React.FC = ({
{/* 2. HP Bars (Character Sheet Style) - Staggered Lines */} -
+
+ + {/* Floating Text - Enemy (Left 60%) */} +
+ {floatingTexts.filter(ft => ft.origin === 'enemy').map(ft => ( +
+ {ft.text} +
+ ))} +
+ + {/* Floating Text - Player (Right 60%) */} +
+ {floatingTexts.filter(ft => ft.origin === 'player').map(ft => ( +
+ {ft.text} +
+ ))} +
{/* Enemy HP (Left) */}
-
- {floatingTexts.filter(ft => ft.origin === 'enemy').map(ft => ( -
- {ft.text} -
- ))} -
= ({ {/* Player HP (Right) */}
-
- {floatingTexts.filter(ft => ft.origin === 'player').map(ft => ( -
- {ft.text} -
- ))} -
(() => { + return (localStorage.getItem('inventoryViewMode') as 'list' | 'grid') || 'list'; + }); + + // Dropdown State + const [activeDropdown, setActiveDropdown] = useState(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 = ( +
+
+ {item.emoji} {getTranslatedText(item.name)} +
+ {item.description &&
{getTranslatedText(item.description)}
} + +
+
āš–ļø {item.weight}kg {item.quantity > 1 && `(x${item.quantity})`}
+
šŸ“¦ {item.volume}L {item.quantity > 1 && `(x${item.quantity})`}
+
+ + {/* Stats Row - Button-like Badges */} +
+ {/* Capacity */} + {(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && ( + + āš–ļø +{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg + + )} + {(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && ( + + šŸ“¦ +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L + + )} + + {/* Combat */} + {(item.unique_stats?.damage_min || item.stats?.damage_min) && ( + + āš”ļø {item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max} + + )} + {(item.unique_stats?.armor || item.stats?.armor) && ( + + šŸ›”ļø +{item.unique_stats?.armor || item.stats?.armor} + + )} + {(item.unique_stats?.armor_penetration || item.stats?.armor_penetration) && ( + + šŸ’” +{item.unique_stats?.armor_penetration || item.stats?.armor_penetration} {t('stats.pen')} + + )} + {(item.unique_stats?.crit_chance || item.stats?.crit_chance) && ( + + šŸŽÆ +{Math.round((item.unique_stats?.crit_chance || item.stats?.crit_chance) * 100)}% {t('stats.crit')} + + )} + {(item.unique_stats?.accuracy || item.stats?.accuracy) && ( + + šŸ‘ļø +{Math.round((item.unique_stats?.accuracy || item.stats?.accuracy) * 100)}% {t('stats.acc')} + + )} + {(item.unique_stats?.dodge_chance || item.stats?.dodge_chance) && ( + + šŸ’Ø +{Math.round((item.unique_stats?.dodge_chance || item.stats?.dodge_chance) * 100)}% Dodge + + )} + {(item.unique_stats?.lifesteal || item.stats?.lifesteal) && ( + + šŸ§› +{Math.round((item.unique_stats?.lifesteal || item.stats?.lifesteal) * 100)}% {t('stats.life')} + + )} + + {/* Attributes */} + {(item.unique_stats?.strength_bonus || item.stats?.strength_bonus) && ( + + šŸ’Ŗ +{item.unique_stats?.strength_bonus || item.stats?.strength_bonus} {t('stats.str')} + + )} + {(item.unique_stats?.agility_bonus || item.stats?.agility_bonus) && ( + + šŸƒ +{item.unique_stats?.agility_bonus || item.stats?.agility_bonus} {t('stats.agi')} + + )} + {(item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus) && ( + + šŸ‹ļø +{item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus} {t('stats.end')} + + )} + {(item.unique_stats?.hp_bonus || item.stats?.hp_bonus) && ( + + ā¤ļø +{item.unique_stats?.hp_bonus || item.stats?.hp_bonus} {t('stats.hpMax')} + + )} + {(item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus) && ( + + ⚔ +{item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus} {t('stats.stmMax')} + + )} + + {/* Consumables */} + {item.hp_restore && ( + + ā¤ļø +{item.hp_restore} HP + + )} + {item.stamina_restore && ( + + ⚔ +{item.stamina_restore} Stm + + )} + + {/* Status Effects */} + {item.effects?.status_effect && ( + + )} + + {item.effects?.cures && item.effects.cures.length > 0 && ( + + šŸ’Š {t('game.cures')}: {item.effects.cures.map((c: string) => t(`game.effects.${c}`, c)).join(', ')} + + )} + +
+ + {/* Durability Bar */} + {hasDurability && ( +
+
+ {t('game.durability')} + + {currentDurability} / {maxDurability} + +
+
+
+
+
+ )} +
+ ); + + return ( +
+ +
handleItemClick(e, item)} + > + {/* Image/Icon */} +
+ {item.image_path ? ( + {getTranslatedText(item.name)} { + (e.target as HTMLImageElement).style.display = 'none'; + const icon = (e.target as HTMLImageElement).nextElementSibling; + if (icon) icon.classList.remove('hidden'); + }} + /> + ) : null} +
+ {item.emoji || 'šŸ“¦'} +
+
+ + {/* Quantity Badge */} + {item.quantity > 1 &&
x{item.quantity}
} + + {/* Equipped Indicator */} + {item.is_equipped &&
E
} +
+
+ + {/* Dropdown Menu */} + {activeDropdown === item.id && ( + setActiveDropdown(null)} + position={dropdownPos} + width="180px" + > +
+ {getTranslatedText(item.name)} +
+ + {item.consumable && ( + { + if (!isEffectActive) { + playSfx('/audio/sfx/use.wav'); + onUseItem(item.item_id, item.id); + setActiveDropdown(null); + } + }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + {t('game.use')} + + )} + + {item.equippable && !item.is_equipped && ( + { + playSfx('/audio/sfx/equip.wav'); + onEquipItem(item.id); + setActiveDropdown(null); + }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + {t('game.equip')} + + )} + + {item.is_equipped && ( + { + playSfx('/audio/sfx/unequip.wav'); + onUnequipItem(item.slot); + setActiveDropdown(null); + }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + {t('game.unequip')} + + )} + + {(item.consumable || (item.equippable && !item.is_equipped) || item.is_equipped) && +
+ } + + { + playSfx('/audio/sfx/drop.wav'); + onDropItem(item.item_id, item.id, 1); + setActiveDropdown(null); + }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + {t('game.drop')} (x1) + + + {item.quantity >= 5 && ( + { + playSfx('/audio/sfx/drop.wav'); + onDropItem(item.item_id, item.id, 5); + setActiveDropdown(null); + }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + {t('game.drop')} (x5) + + )} + + {item.quantity > 1 && ( + { + 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')}) + + )} + + + )} +
+ ); + }; + return (
) => { if (e.target === e.currentTarget) handleClose() @@ -437,6 +775,17 @@ function InventoryModal({ value={inventoryFilter} onChange={(e: ChangeEvent) => onSetInventoryFilter(e.target.value)} /> + + {/* View Mode Toggle */} +
+ +
@@ -452,14 +801,17 @@ function InventoryModal({ {filteredItems.some((item: any) => item.is_equipped) && ( <>
āš”ļø {t('game.equipped')}
- {filteredItems.filter((item: any) => item.is_equipped).map((item: any, i: number) => renderItemCard(item, i))} +
+ {filteredItems.filter((item: any) => item.is_equipped).map((item: any, i: number) => + viewMode === 'list' ? renderItemCard(item, i) : renderGridItem(item, i) + )} +
)} {/* Backpack - grouped by categories */} {filteredItems.some((item: any) => !item.is_equipped) && ( <> -
šŸŽ’ {t('game.backpack')}
{/* 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 (
-
+
{cat.icon} {cat.label} ({categoryItems.length})
- {categoryItems.map((item: any, i: number) => renderItemCard(item, i))} +
+ {categoryItems.map((item: any, i: number) => + viewMode === 'list' ? renderItemCard(item, i) : renderGridItem(item, i) + )} +
); })} @@ -484,7 +840,11 @@ function InventoryModal({ ) : ( /* Single category */ - filteredItems.map((item: any, i: number) => renderItemCard(item, i)) +
+ {filteredItems.map((item: any, i: number) => + viewMode === 'list' ? renderItemCard(item, i) : renderGridItem(item, i) + )} +
) )}
diff --git a/pwa/src/components/game/LocationView.tsx b/pwa/src/components/game/LocationView.tsx index 3ad4050..5d26a40 100644 --- a/pwa/src/components/game/LocationView.tsx +++ b/pwa/src/components/game/LocationView.tsx @@ -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({
- {message && ( + {/* {message && (
onSetMessage('')}> {message}
- )} + )} */} {locationMessages.length > 0 && (
@@ -313,8 +315,10 @@ function LocationView({

{t('location.itemsOnGround')}

{location.items.map((item: any, i: number) => { + // Use loose equality to handle potential string/number mismatches + const isShaking = failedActionItemId == item.id; return ( -
+
{item.description &&
{getTranslatedText(item.description)}
} @@ -377,7 +381,7 @@ function LocationView({ diff --git a/pwa/src/components/game/Workbench.css b/pwa/src/components/game/Workbench.css index 3352f1f..c7c1dca 100644 --- a/pwa/src/components/game/Workbench.css +++ b/pwa/src/components/game/Workbench.css @@ -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); diff --git a/pwa/src/components/game/game_pickup.css b/pwa/src/components/game/game_pickup.css index 03c8775..3d705d4 100644 --- a/pwa/src/components/game/game_pickup.css +++ b/pwa/src/components/game/game_pickup.css @@ -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; } diff --git a/pwa/src/components/game/hooks/useGameEngine.ts b/pwa/src/components/game/hooks/useGameEngine.ts index 69fbf5e..12aca9d 100644 --- a/pwa/src/components/game/hooks/useGameEngine.ts +++ b/pwa/src/components/game/hooks/useGameEngine.ts @@ -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 ): [GameEngineState, GameEngineActions] { const { t } = useTranslation() + const { playSfx } = useAudio() // All state declarations const [playerState, setPlayerState] = useState(null) const [location, setLocation] = useState(null) @@ -160,6 +163,7 @@ export function useGameEngine( const [expandedCorpse, setExpandedCorpse] = useState(null) const [corpseDetails, setCorpseDetails] = useState(null) const [movementCooldown, setMovementCooldown] = useState(0) + const [failedActionItemId, setFailedActionItemId] = useState(null) // const [enemyTurnMessage, setEnemyTurnMessage] = useState('') // Moved to Combat.tsx const [equipment, setEquipment] = useState({}) @@ -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') } } diff --git a/pwa/src/index.css b/pwa/src/index.css index 6bec3ac..de05416 100644 --- a/pwa/src/index.css +++ b/pwa/src/index.css @@ -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 {