1080p layout fixes: responsive location sizing, dynamic entity limits, compass cleanup
@@ -54,8 +54,15 @@ for category in items locations npcs interactables characters placeholder static
|
|||||||
rm "$tmp"
|
rm "$tmp"
|
||||||
else
|
else
|
||||||
# Standard conversion for other categories
|
# Standard conversion for other categories
|
||||||
|
# If locations or interactables, crop to 16:9
|
||||||
|
tmp="/tmp/${base}_clean.png"
|
||||||
|
if [[ "$category" == "locations" || "$category" == "interactables" ]]; then
|
||||||
|
convert "$img" -resize "16:9" "$tmp"
|
||||||
|
else
|
||||||
|
convert "$img" "$tmp"
|
||||||
|
fi
|
||||||
echo " ➜ Converting: $filename"
|
echo " ➜ Converting: $filename"
|
||||||
cwebp "$img" -q 85 -o "$out_file" >/dev/null
|
cwebp "$tmp" -q 85 -o "$out_file" >/dev/null
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 154 KiB |
@@ -1,16 +1,17 @@
|
|||||||
html {
|
html {
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
/* Always show scrollbar to prevent layout shift */
|
/* Lock the page — no document-level scrollbar */
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-container {
|
.game-container {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--game-bg-app);
|
background: var(--game-bg-app);
|
||||||
color: var(--game-text-primary);
|
color: var(--game-text-primary);
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: var(--game-font-main);
|
font-family: var(--game-font-main);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Death Overlay */
|
/* Death Overlay */
|
||||||
@@ -317,7 +318,8 @@ html {
|
|||||||
.game-main {
|
.game-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unified Search Bar */
|
/* Unified Search Bar */
|
||||||
@@ -365,7 +367,6 @@ html {
|
|||||||
.explore-tab-desktop {
|
.explore-tab-desktop {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 380px 1fr 380px;
|
grid-template-columns: 380px 1fr 380px;
|
||||||
gap: 1.5rem;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -374,9 +375,9 @@ html {
|
|||||||
.left-sidebar {
|
.left-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: var(--game-gap-md);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 0.5rem;
|
padding-right: var(--game-padding-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center Content */
|
/* Center Content */
|
||||||
@@ -387,13 +388,21 @@ html {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Center Area (combat/location container) */
|
||||||
|
.center-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Right Sidebar */
|
/* Right Sidebar */
|
||||||
.right-sidebar {
|
.right-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: var(--game-gap-md);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-left: 0.5rem;
|
padding-left: var(--game-padding-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile fallback */
|
/* Mobile fallback */
|
||||||
@@ -411,11 +420,14 @@ html {
|
|||||||
|
|
||||||
.location-info {
|
.location-info {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
padding: 1.5rem;
|
padding: var(--game-padding-md);
|
||||||
margin-bottom: 1.5rem;
|
gap: var(--game-gap-md);
|
||||||
|
margin-bottom: 0;
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
box-shadow: var(--game-shadow-card);
|
box-shadow: var(--game-shadow-card);
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-info h2 {
|
.location-info h2 {
|
||||||
@@ -425,7 +437,7 @@ html {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: var(--game-gap-md);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
@@ -683,18 +695,19 @@ html {
|
|||||||
|
|
||||||
.location-image-container {
|
.location-image-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: var(--location-content-width);
|
||||||
margin: 1rem auto;
|
margin: 0 auto;
|
||||||
aspect-ratio: 10 / 7;
|
aspect-ratio: 16 / 9;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 2px solid rgba(255, 107, 107, 0.3);
|
border: 2px solid rgba(255, 107, 107, 0.3);
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-image {
|
.location-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-box {
|
.message-box {
|
||||||
@@ -709,8 +722,8 @@ html {
|
|||||||
.location-messages-log {
|
.location-messages-log {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
padding: 0.8rem;
|
padding: 0.5rem 0.8rem;
|
||||||
margin-top: 1rem;
|
margin-top: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
}
|
}
|
||||||
@@ -761,7 +774,7 @@ html {
|
|||||||
|
|
||||||
.movement-controls {
|
.movement-controls {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
padding: 1.5rem;
|
padding: var(--game-padding-md);
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
box-shadow: var(--game-shadow-card);
|
box-shadow: var(--game-shadow-card);
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
@@ -781,7 +794,7 @@ html {
|
|||||||
grid-template-columns: repeat(3, 80px);
|
grid-template-columns: repeat(3, 80px);
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
max-width: 260px;
|
max-width: 260px;
|
||||||
margin: 0 auto 1rem auto;
|
margin: 0 auto 0 auto;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,14 +1048,18 @@ html {
|
|||||||
/* Interactables Section */
|
/* Interactables Section */
|
||||||
.interactables-section {
|
.interactables-section {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
padding: 1.5rem;
|
padding: var(--game-padding-md);
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--game-gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.interactables-section h3 {
|
.interactables-section h3 {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 0.3rem 0;
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.no-scroll {
|
body.no-scroll {
|
||||||
@@ -1052,7 +1069,6 @@ body.no-scroll {
|
|||||||
/* Interactable Card with Image */
|
/* Interactable Card with Image */
|
||||||
.interactable-card {
|
.interactable-card {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 1px solid rgba(255, 193, 7, 0.4);
|
border: 1px solid rgba(255, 193, 7, 0.4);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1069,7 +1085,7 @@ body.no-scroll {
|
|||||||
|
|
||||||
.interactable-image-container {
|
.interactable-image-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 10 / 7;
|
aspect-ratio: 16 / 9;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1080,7 +1096,7 @@ body.no-scroll {
|
|||||||
.interactable-image {
|
.interactable-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1140,39 +1156,32 @@ body.no-scroll {
|
|||||||
|
|
||||||
/* Ground Entities - NPCs and Items */
|
/* Ground Entities - NPCs and Items */
|
||||||
.ground-entities {
|
.ground-entities {
|
||||||
margin-top: 1.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: var(--game-gap-md);
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-section {
|
.entity-section {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
padding: 1.5rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-section h3 {
|
.entity-section h3 {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 0.3rem 0;
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
font-size: 1.2rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-list {
|
.entity-list:not(.grid-view) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
max-height: 400px;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 5px;
|
|
||||||
/* Custom scrollbar */
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--game-border-active) rgba(0, 0, 0, 0.3);
|
|
||||||
/* Ensure overflow doesn't clip dropdowns if possible,
|
|
||||||
but for scrolling lists we need overflow-y: auto.
|
|
||||||
Dropdowns must use fixed position or Portal if they need to escape.
|
|
||||||
However, we can try to make sure cards have z-index context */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure cards handle their own stacking context */
|
/* Ensure cards handle their own stacking context */
|
||||||
@@ -1735,20 +1744,20 @@ body.no-scroll {
|
|||||||
/* Profile Sidebar */
|
/* Profile Sidebar */
|
||||||
.profile-sidebar {
|
.profile-sidebar {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
padding: 1rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-sidebar h3 {
|
.profile-sidebar h3 {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 0.3rem 0;
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-stat-bars {
|
.sidebar-stat-bars {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-stat-bar {
|
.sidebar-stat-bar {
|
||||||
@@ -1960,17 +1969,16 @@ body.no-scroll {
|
|||||||
/* Equipment Sidebar */
|
/* Equipment Sidebar */
|
||||||
.equipment-sidebar {
|
.equipment-sidebar {
|
||||||
background: var(--game-bg-panel);
|
background: var(--game-bg-panel);
|
||||||
padding: 1rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
/* Allow tooltips to overflow */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-sidebar h3 {
|
.equipment-sidebar h3 {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 0.3rem 0;
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3516,14 +3524,13 @@ body.no-scroll {
|
|||||||
|
|
||||||
/* Combat Log Styles */
|
/* Combat Log Styles */
|
||||||
.combat-log-container {
|
.combat-log-container {
|
||||||
margin-top: 2rem;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
border: 2px solid rgba(244, 67, 54, 0.3);
|
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||||
padding: 1rem;
|
padding: var(--game-padding-md);
|
||||||
max-width: 800px;
|
width: var(--location-content-width);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
clip-path: var(--game-clip-path-sm);
|
clip-path: var(--game-clip-path);
|
||||||
}
|
}
|
||||||
|
|
||||||
.combat-log-container h4 {
|
.combat-log-container h4 {
|
||||||
@@ -3657,10 +3664,9 @@ body.no-scroll {
|
|||||||
.location-description-box {
|
.location-description-box {
|
||||||
background: rgba(25, 26, 31, 0.6);
|
background: rgba(25, 26, 31, 0.6);
|
||||||
border: 1px solid rgba(107, 185, 240, 0.3);
|
border: 1px solid rgba(107, 185, 240, 0.3);
|
||||||
padding: 1rem;
|
padding: var(--game-padding-md);
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: var(--location-content-width);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -4320,7 +4326,7 @@ body.no-scroll {
|
|||||||
.combat-actions {
|
.combat-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: var(--game-gap-md);
|
||||||
/* min-height removed to fit content */
|
/* min-height removed to fit content */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* Center content vertically */
|
/* Center content vertically */
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuth } from '../hooks/useAuth'
|
import { useAuth } from '../hooks/useAuth'
|
||||||
import { useGameWebSocket } from '../hooks/useGameWebSocket'
|
import { useGameWebSocket } from '../hooks/useGameWebSocket'
|
||||||
@@ -34,6 +34,28 @@ export default function GameHeader({
|
|||||||
const { currentCharacter, logout } = useAuth()
|
const { currentCharacter, logout } = useAuth()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [playerCount, setPlayerCount] = useState<number>(0)
|
const [playerCount, setPlayerCount] = useState<number>(0)
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState<boolean>(!!document.fullscreenElement)
|
||||||
|
|
||||||
|
// Fullscreen toggle
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch((err) => {
|
||||||
|
console.error('Failed to enter fullscreen:', err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen().catch((err) => {
|
||||||
|
console.error('Failed to exit fullscreen:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
setIsFullscreen(!!document.fullscreenElement)
|
||||||
|
}
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Get game state from context (undefined when outside GameProvider)
|
// Get game state from context (undefined when outside GameProvider)
|
||||||
const gameContext = useOptionalGame()
|
const gameContext = useOptionalGame()
|
||||||
@@ -149,6 +171,16 @@ export default function GameHeader({
|
|||||||
|
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|
||||||
|
<GameTooltip content={isFullscreen ? t('common.exitFullscreen', 'Exit Fullscreen') : t('common.fullscreen', 'Fullscreen')}>
|
||||||
|
<button
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
className="header-icon-btn"
|
||||||
|
aria-label={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
|
||||||
|
>
|
||||||
|
{isFullscreen ? '🗗' : '🗖'}
|
||||||
|
</button>
|
||||||
|
</GameTooltip>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
|
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
|
||||||
className={`username-link ${isOnOwnProfile ? 'active' : ''}`}
|
className={`username-link ${isOnOwnProfile ? 'active' : ''}`}
|
||||||
|
|||||||
@@ -1,19 +1,4 @@
|
|||||||
/* Combat Layout */
|
/* Combat Layout */
|
||||||
.combat-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
/* More transparent/themed background */
|
|
||||||
background: rgba(20, 20, 20, 0.6);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
padding: 1rem;
|
|
||||||
color: white;
|
|
||||||
position: relative;
|
|
||||||
/* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-effect {
|
.glow-effect {
|
||||||
box-shadow: 0 0 10px #ff4444, 0 0 20px #ff4444;
|
box-shadow: 0 0 10px #ff4444, 0 0 20px #ff4444;
|
||||||
@@ -25,16 +10,29 @@
|
|||||||
transition: filter 1s ease;
|
transition: filter 1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.combat-main-content {
|
||||||
|
background: var(--game-bg-panel);
|
||||||
|
padding: var(--game-padding-md);
|
||||||
|
gap: var(--game-gap-md);
|
||||||
|
margin-bottom: 0;
|
||||||
|
border: 1px solid var(--game-border-color);
|
||||||
|
box-shadow: var(--game-shadow-card);
|
||||||
|
clip-path: var(--game-clip-path);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
/* Combat Scene: Location Background with NPC Overlay */
|
/* Combat Scene: Location Background with NPC Overlay */
|
||||||
.combat-scene-container {
|
.combat-scene-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: var(--location-content-width);
|
||||||
margin: 1rem auto;
|
margin: 0 auto;
|
||||||
aspect-ratio: 10 / 7;
|
aspect-ratio: 16 / 9;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip-path: var(--game-clip-path);
|
clip-path: var(--game-clip-path);
|
||||||
border: 2px solid rgba(255, 107, 107, 0.3);
|
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.combat-location-bg {
|
.combat-location-bg {
|
||||||
@@ -497,10 +495,9 @@
|
|||||||
.combat-stats-container {
|
.combat-stats-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: var(--game-gap-md);
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: var(--location-content-width);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,3 +100,8 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Entity "Show All" modal - wider like inventory */
|
||||||
|
.game-modal-container.entity-show-all-modal {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
@@ -1,15 +1,84 @@
|
|||||||
|
/* Location View Layout */
|
||||||
|
.location-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: var(--game-gap-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-view .location-info {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-view .location-description-box {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-view .location-messages-log {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Show All" card for overflowing entity sections */
|
||||||
|
.entity-show-more-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px dashed var(--game-border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--game-text-secondary);
|
||||||
|
gap: 0.3rem;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-show-more-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: var(--game-text-highlight);
|
||||||
|
color: var(--game-text-highlight);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-show-more-card .show-more-count {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-show-more-card .show-more-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entity modal - scrollable grid */
|
||||||
|
.entity-modal-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||||
|
grid-auto-rows: max-content;
|
||||||
|
align-content: start;
|
||||||
|
gap: var(--game-gap-md);
|
||||||
|
padding: var(--game-gap-md);
|
||||||
|
}
|
||||||
|
|
||||||
/* Grid View Styles */
|
/* Grid View Styles */
|
||||||
|
|
||||||
.entities-container {
|
.entities-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1.5rem;
|
gap: 0.5rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-section {
|
.entity-section {
|
||||||
flex: 1 1 300px;
|
flex: 1 1 calc(50% - 0.25rem);
|
||||||
|
min-width: calc(50% - 0.25rem);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ground item images */
|
/* Ground item images */
|
||||||
@@ -28,12 +97,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.entity-list.grid-view {
|
.entity-list.grid-view {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
flex-wrap: nowrap;
|
||||||
gap: 1rem;
|
gap: 0.4rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 0.5rem;
|
padding: 0.25rem;
|
||||||
/* Ensure padding inside container */
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-list.grid-view>* {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-list.grid-view>.entity-show-more-card {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Padded Image for enemies */
|
/* Padded Image for enemies */
|
||||||
@@ -110,21 +190,25 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
aspect-ratio: 1;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* Remove padding to let image fill */
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
border: 1px solid var(--game-border-color);
|
border: 1px solid var(--game-border-color);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
/* User requested NO radius */
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: auto;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar capacity row */
|
||||||
|
.sidebar-capacity-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-card.grid-card:hover {
|
.entity-card.grid-card:hover {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
|
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAudio } from '../../contexts/AudioContext'
|
import { useAudio } from '../../contexts/AudioContext'
|
||||||
import Workbench from './Workbench'
|
import Workbench from './Workbench'
|
||||||
@@ -11,8 +11,23 @@ import { getTranslatedText } from '../../utils/i18nUtils'
|
|||||||
import { DialogModal } from './DialogModal'
|
import { DialogModal } from './DialogModal'
|
||||||
import { TradeModal } from './TradeModal'
|
import { TradeModal } from './TradeModal'
|
||||||
import { ItemTooltipContent } from '../common/ItemTooltipContent'
|
import { ItemTooltipContent } from '../common/ItemTooltipContent'
|
||||||
|
import { GameModal } from './GameModal'
|
||||||
import './LocationView.css'
|
import './LocationView.css'
|
||||||
|
|
||||||
|
const CARD_SIZE = 90; // px - must match CSS
|
||||||
|
const CARD_GAP = 6.4; // px - 0.4rem at default 16px font
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate how many cards fit in a container of given width.
|
||||||
|
* Returns the max number that fit in one row.
|
||||||
|
*/
|
||||||
|
function calcFitCount(containerWidth: number): number {
|
||||||
|
if (containerWidth <= 0) return 3; // fallback
|
||||||
|
// First card doesn't need a gap before it
|
||||||
|
return Math.max(1, Math.floor((containerWidth + CARD_GAP) / (CARD_SIZE + CARD_GAP)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface LocationViewProps {
|
interface LocationViewProps {
|
||||||
location: Location
|
location: Location
|
||||||
playerState: PlayerState | null
|
playerState: PlayerState | null
|
||||||
@@ -101,6 +116,36 @@ function LocationView({
|
|||||||
// Dropdown State
|
// Dropdown State
|
||||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null)
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Entity "Show All" Modal State
|
||||||
|
const [entityModal, setEntityModal] = useState<{ type: string; title: string } | null>(null)
|
||||||
|
|
||||||
|
// Measure entity-list container widths for dynamic fit
|
||||||
|
const [sectionWidths, setSectionWidths] = useState<Record<string, number>>({});
|
||||||
|
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
|
|
||||||
|
const measureSections = useCallback(() => {
|
||||||
|
const newWidths: Record<string, number> = {};
|
||||||
|
Object.entries(sectionRefs.current).forEach(([key, el]) => {
|
||||||
|
if (el) newWidths[key] = el.clientWidth;
|
||||||
|
});
|
||||||
|
setSectionWidths(newWidths);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
measureSections();
|
||||||
|
const observer = new ResizeObserver(() => measureSections());
|
||||||
|
Object.values(sectionRefs.current).forEach(el => {
|
||||||
|
if (el) observer.observe(el);
|
||||||
|
});
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [measureSections, location]);
|
||||||
|
|
||||||
|
const getSectionRef = useCallback((key: string) => (el: HTMLDivElement | null) => {
|
||||||
|
sectionRefs.current[key] = el;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getFitCount = (sectionKey: string) => calcFitCount(sectionWidths[sectionKey] || 0);
|
||||||
|
|
||||||
// NPC Interaction State
|
// NPC Interaction State
|
||||||
const [activeDialogNpc, setActiveDialogNpc] = useState<string | null>(null)
|
const [activeDialogNpc, setActiveDialogNpc] = useState<string | null>(null)
|
||||||
const [showTradeModal, setShowTradeModal] = useState<boolean>(false)
|
const [showTradeModal, setShowTradeModal] = useState<boolean>(false)
|
||||||
@@ -299,22 +344,26 @@ function LocationView({
|
|||||||
{/* Combined Entities Container for Grid Layout */}
|
{/* Combined Entities Container for Grid Layout */}
|
||||||
<div className="entities-container">
|
<div className="entities-container">
|
||||||
{/* Enemies */}
|
{/* Enemies */}
|
||||||
{location.npcs.filter((npc: any) => npc.type === 'enemy').length > 0 && (
|
{(() => {
|
||||||
|
const enemies = location.npcs.filter((npc: any) => npc.type === 'enemy');
|
||||||
|
if (enemies.length === 0) return null;
|
||||||
|
const limit = getFitCount('enemies');
|
||||||
|
const visibleEnemies = enemies.length > limit ? enemies.slice(0, limit - 1) : enemies;
|
||||||
|
const hasMore = enemies.length > limit;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="entity-section enemies-section">
|
<div className="entity-section enemies-section">
|
||||||
<h3>{t('location.enemies')}</h3>
|
<h3>{t('location.enemies')}</h3>
|
||||||
<div className="entity-list grid-view">
|
<div className="entity-list grid-view" ref={getSectionRef('enemies')}>
|
||||||
{location.npcs.filter((npc: any) => npc.type === 'enemy').map((enemy: any) => {
|
{visibleEnemies.map((enemy: any) => {
|
||||||
const isShaking = failedActionItemId == enemy.id;
|
const isShaking = failedActionItemId == enemy.id;
|
||||||
const id = `enemy-${enemy.id}`;
|
const id = `enemy-${enemy.id}`;
|
||||||
|
|
||||||
// Only render if valid
|
|
||||||
if (!enemy || !enemy.id) return null;
|
if (!enemy || !enemy.id) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={enemy.id} className={`entity-card enemy-card grid-card ${isShaking ? 'shake-animation' : ''}`}
|
<div key={enemy.id} className={`entity-card enemy-card grid-card ${isShaking ? 'shake-animation' : ''}`}
|
||||||
onClick={(e) => handleDropdownClick(e, id)}
|
onClick={(e) => handleDropdownClick(e, id)}
|
||||||
>
|
>
|
||||||
{/* Enemy Image */}
|
|
||||||
{enemy.id && (
|
{enemy.id && (
|
||||||
<div className="entity-image padded-image">
|
<div className="entity-image padded-image">
|
||||||
<img
|
<img
|
||||||
@@ -335,7 +384,6 @@ function LocationView({
|
|||||||
<div className="grid-overlay"></div>
|
<div className="grid-overlay"></div>
|
||||||
</GameTooltip>
|
</GameTooltip>
|
||||||
|
|
||||||
{/* Dropdown for Grid View */}
|
|
||||||
{activeDropdown === id && (
|
{activeDropdown === id && (
|
||||||
<GameDropdown
|
<GameDropdown
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
@@ -360,16 +408,30 @@ function LocationView({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
{hasMore && (
|
||||||
|
<div className="entity-show-more-card" onClick={() => setEntityModal({ type: 'enemies', title: t('location.enemies') })}>
|
||||||
|
<span className="show-more-count">+{enemies.length - (limit - 1)}</span>
|
||||||
|
<span className="show-more-label">{t('common.showAll', 'Show All')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Corpses */}
|
{/* Corpses */}
|
||||||
{location.corpses && location.corpses.length > 0 && (
|
{(() => {
|
||||||
|
const corpses = location.corpses || [];
|
||||||
|
if (corpses.length === 0) return null;
|
||||||
|
const limit = getFitCount('corpses');
|
||||||
|
const visibleCorpses = corpses.length > limit ? corpses.slice(0, limit - 1) : corpses;
|
||||||
|
const hasMore = corpses.length > limit;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="entity-section corpses-section">
|
<div className="entity-section corpses-section">
|
||||||
<h3>{t('location.corpses')}</h3>
|
<h3>{t('location.corpses')}</h3>
|
||||||
<div className="entity-list grid-view">
|
<div className="entity-list grid-view" ref={getSectionRef('corpses')}>
|
||||||
{location.corpses.map((corpse: any) => (
|
{visibleCorpses.map((corpse: any) => (
|
||||||
<div key={corpse.id} className="corpse-container">
|
<div key={corpse.id} className="corpse-container">
|
||||||
<div className="entity-card corpse-card grid-card"
|
<div className="entity-card corpse-card grid-card"
|
||||||
onClick={(e) => handleDropdownClick(e, `corpse-${corpse.id}`)}
|
onClick={(e) => handleDropdownClick(e, `corpse-${corpse.id}`)}
|
||||||
@@ -424,16 +486,30 @@ function LocationView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
{hasMore && (
|
||||||
|
<div className="entity-show-more-card" onClick={() => setEntityModal({ type: 'corpses', title: t('location.corpses') })}>
|
||||||
|
<span className="show-more-count">+{corpses.length - (limit - 1)}</span>
|
||||||
|
<span className="show-more-label">{t('common.showAll', 'Show All')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Friendly NPCs */}
|
{/* Friendly NPCs */}
|
||||||
{location.npcs.filter((npc: any) => npc.type !== 'enemy').length > 0 && (
|
{(() => {
|
||||||
|
const friendlyNpcs = location.npcs.filter((npc: any) => npc.type !== 'enemy');
|
||||||
|
if (friendlyNpcs.length === 0) return null;
|
||||||
|
const limit = getFitCount('npcs');
|
||||||
|
const visibleNpcs = friendlyNpcs.length > limit ? friendlyNpcs.slice(0, limit - 1) : friendlyNpcs;
|
||||||
|
const hasMore = friendlyNpcs.length > limit;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="entity-section npcs-section">
|
<div className="entity-section npcs-section">
|
||||||
<h3>{t('location.npcs')}</h3>
|
<h3>{t('location.npcs')}</h3>
|
||||||
<div className="entity-list grid-view">
|
<div className="entity-list grid-view" ref={getSectionRef('npcs')}>
|
||||||
{location.npcs.filter((npc: any) => npc.type !== 'enemy').map((npc: any, i: number) => (
|
{visibleNpcs.map((npc: any, i: number) => (
|
||||||
<div key={i} className="entity-card npc-card grid-card"
|
<div key={i} className="entity-card npc-card grid-card"
|
||||||
onClick={() => handleNpcClick(npc)}
|
onClick={() => handleNpcClick(npc)}
|
||||||
style={{ cursor: 'pointer', position: 'relative' }}
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
@@ -456,22 +532,29 @@ function LocationView({
|
|||||||
</GameTooltip>
|
</GameTooltip>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
{hasMore && (
|
||||||
|
<div className="entity-show-more-card" onClick={() => setEntityModal({ type: 'npcs', title: t('location.npcs') })}>
|
||||||
|
<span className="show-more-count">+{friendlyNpcs.length - (limit - 1)}</span>
|
||||||
|
<span className="show-more-label">{t('common.showAll', 'Show All')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Items on Ground - Stable Sort */}
|
{/* Items on Ground - Stable Sort */}
|
||||||
{location.items.length > 0 && (
|
{(() => {
|
||||||
<div className="entity-section items-section">
|
const sortedItems = [...location.items].sort((a: any, b: any) => (a.id || 0) - (b.id || 0));
|
||||||
<h3>{t('location.itemsOnGround')}</h3>
|
if (sortedItems.length === 0) return null;
|
||||||
<div className="entity-list grid-view">
|
const limit = getFitCount('items');
|
||||||
{[...location.items]
|
const visibleItems = sortedItems.length > limit ? sortedItems.slice(0, limit - 1) : sortedItems;
|
||||||
.sort((a: any, b: any) => (a.id || 0) - (b.id || 0))
|
const hasMore = sortedItems.length > limit;
|
||||||
.map((item: any, i: number) => {
|
|
||||||
|
const renderItemCard = (item: any, i: number) => {
|
||||||
const isShaking = failedActionItemId == item.id;
|
const isShaking = failedActionItemId == item.id;
|
||||||
const itemId = `item-${item.id}-${i}`;
|
const itemId = `item-${item.id}-${i}`;
|
||||||
|
|
||||||
// Pickup Options Helper - Vertical Layout
|
|
||||||
const renderPickupOptions = () => {
|
const renderPickupOptions = () => {
|
||||||
const options = [];
|
const options = [];
|
||||||
options.push({ label: `${t('common.pickUp')} (x1)`, qty: 1 });
|
options.push({ label: `${t('common.pickUp')} (x1)`, qty: 1 });
|
||||||
@@ -536,11 +619,10 @@ function LocationView({
|
|||||||
<GameDropdown
|
<GameDropdown
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onClose={() => setActiveDropdown(null)}
|
onClose={() => setActiveDropdown(null)}
|
||||||
width="200px" // Wider for split buttons
|
width="200px"
|
||||||
>
|
>
|
||||||
<div className="game-dropdown-header">{getTranslatedText(item.name)} {item.quantity > 1 ? `(x${item.quantity})` : ''}</div>
|
<div className="game-dropdown-header">{getTranslatedText(item.name)} {item.quantity > 1 ? `(x${item.quantity})` : ''}</div>
|
||||||
|
|
||||||
{/* Primary Action: Pick Up 1 */}
|
|
||||||
<GameButton
|
<GameButton
|
||||||
variant="success"
|
variant="success"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -555,7 +637,6 @@ function LocationView({
|
|||||||
✋ {t('common.pickUp')}
|
✋ {t('common.pickUp')}
|
||||||
</GameButton>
|
</GameButton>
|
||||||
|
|
||||||
{/* Quantity Options if > 1 */}
|
|
||||||
{item.quantity > 1 && (
|
{item.quantity > 1 && (
|
||||||
<>
|
<>
|
||||||
<div className="game-dropdown-divider" style={{ margin: '8px 0' }} />
|
<div className="game-dropdown-divider" style={{ margin: '8px 0' }} />
|
||||||
@@ -566,17 +647,33 @@ function LocationView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<div className="entity-section items-section">
|
||||||
|
<h3>{t('location.items')}</h3>
|
||||||
|
<div className="entity-list grid-view" ref={getSectionRef('items')}>
|
||||||
|
{visibleItems.map((item: any, i: number) => renderItemCard(item, i))}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="entity-show-more-card" onClick={() => setEntityModal({ type: 'items', title: t('location.items') })}>
|
||||||
|
<span className="show-more-count">+{sortedItems.length - (limit - 1)}</span>
|
||||||
|
<span className="show-more-label">{t('common.showAll', 'Show All')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Other Players */}
|
{/* Other Players */}
|
||||||
{location.other_players && location.other_players.length > 0 && (
|
{(() => {
|
||||||
<div className="entity-section players-section">
|
const players = location.other_players || [];
|
||||||
<h3>👥 {t('location.otherPlayers', 'Other Players')}</h3>
|
if (players.length === 0) return null;
|
||||||
<div className="entity-list grid-view">
|
const limit = getFitCount('players');
|
||||||
{location.other_players.map((player: any, i: number) => {
|
const visiblePlayers = players.length > limit ? players.slice(0, limit - 1) : players;
|
||||||
|
const hasMore = players.length > limit;
|
||||||
|
|
||||||
|
const renderPlayerCard = (player: any, i: number) => {
|
||||||
const playerId = `player-${player.id}-${i}`;
|
const playerId = `player-${player.id}-${i}`;
|
||||||
const canPvP = player.can_pvp;
|
const canPvP = player.can_pvp;
|
||||||
|
|
||||||
@@ -597,7 +694,6 @@ function LocationView({
|
|||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="grid-corpse-content">
|
<div className="grid-corpse-content">
|
||||||
{/* Placeholder for player image or avatar */}
|
|
||||||
<div style={{ fontSize: '2.5rem' }}>
|
<div style={{ fontSize: '2.5rem' }}>
|
||||||
🧍
|
🧍
|
||||||
</div>
|
</div>
|
||||||
@@ -645,12 +741,26 @@ function LocationView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<div className="entity-section players-section">
|
||||||
|
<h3>👥 {t('location.otherPlayers', 'Other Players')}</h3>
|
||||||
|
<div className="entity-list grid-view" ref={getSectionRef('players')}>
|
||||||
|
{visiblePlayers.map((player: any, i: number) => renderPlayerCard(player, i))}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="entity-show-more-card" onClick={() => setEntityModal({ type: 'players', title: t('location.players') })}>
|
||||||
|
<span className="show-more-count">+{players.length - (limit - 1)}</span>
|
||||||
|
<span className="show-more-label">{t('common.showAll', 'Show All')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Corpse Loot Overlay Modal */}
|
{/* Corpse Loot Overlay Modal */}
|
||||||
{
|
{
|
||||||
@@ -774,6 +884,159 @@ function LocationView({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{/* Entity "Show All" Modal */}
|
||||||
|
{entityModal && (
|
||||||
|
<GameModal
|
||||||
|
title={entityModal.title}
|
||||||
|
onClose={() => setEntityModal(null)}
|
||||||
|
className="entity-show-all-modal"
|
||||||
|
>
|
||||||
|
<div className="entity-modal-grid">
|
||||||
|
{entityModal.type === 'enemies' && location.npcs
|
||||||
|
.filter((npc: any) => npc.type === 'enemy')
|
||||||
|
.map((enemy: any) => {
|
||||||
|
const id = `modal-enemy-${enemy.id}`;
|
||||||
|
return (
|
||||||
|
<div key={enemy.id} className="entity-card enemy-card grid-card"
|
||||||
|
onClick={(e) => handleDropdownClick(e, id)}
|
||||||
|
>
|
||||||
|
{enemy.id && (
|
||||||
|
<div className="entity-image padded-image">
|
||||||
|
<img
|
||||||
|
src={getAssetPath(enemy.image_path || `images/npcs/${(typeof enemy.name === 'string' ? enemy.name : enemy.name?.en || '').toLowerCase().replace(/ /g, '_')}.webp`)}
|
||||||
|
alt={getTranslatedText(enemy.name)}
|
||||||
|
onError={(e: any) => { e.currentTarget.style.display = 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<GameTooltip content={
|
||||||
|
<div>
|
||||||
|
<div className="tooltip-title">{getTranslatedText(enemy.name)}</div>
|
||||||
|
<div>{t('location.level')} {enemy.level}</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div className="grid-overlay"></div>
|
||||||
|
</GameTooltip>
|
||||||
|
{activeDropdown === id && (
|
||||||
|
<GameDropdown isOpen={true} onClose={() => setActiveDropdown(null)} width="160px">
|
||||||
|
<div className="game-dropdown-header">{getTranslatedText(enemy.name)}</div>
|
||||||
|
<GameButton variant="danger" size="sm"
|
||||||
|
onClick={() => { onInitiateCombat(enemy.id); setActiveDropdown(null); setEntityModal(null); }}
|
||||||
|
style={{ width: '100%', justifyContent: 'flex-start' }}
|
||||||
|
>
|
||||||
|
⚔️ {t('common.fight')}
|
||||||
|
</GameButton>
|
||||||
|
</GameDropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{entityModal.type === 'corpses' && (location.corpses || []).map((corpse: any) => (
|
||||||
|
<div key={corpse.id} className="entity-card corpse-card grid-card"
|
||||||
|
onClick={(e) => handleDropdownClick(e, `modal-corpse-${corpse.id}`)}
|
||||||
|
>
|
||||||
|
<div className="grid-corpse-content">
|
||||||
|
{corpse.image_path ? (
|
||||||
|
<img src={getAssetPath(corpse.image_path)} alt={getTranslatedText(corpse.name)} className="corpse-image"
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
||||||
|
) : <div style={{ fontSize: '2rem' }}>{corpse.emoji}</div>}
|
||||||
|
<div className="corpse-loot-count">{corpse.loot_count} items</div>
|
||||||
|
</div>
|
||||||
|
{activeDropdown === `modal-corpse-${corpse.id}` && (
|
||||||
|
<GameDropdown isOpen={true} onClose={() => setActiveDropdown(null)} width="160px">
|
||||||
|
<div className="game-dropdown-header">{getTranslatedText(corpse.name)}</div>
|
||||||
|
<GameButton variant="secondary" size="sm"
|
||||||
|
onClick={() => { playSfx('/audio/sfx/interact.wav'); onLootCorpse(String(corpse.id)); setActiveDropdown(null); setEntityModal(null); }}
|
||||||
|
disabled={corpse.loot_count === 0}
|
||||||
|
style={{ width: '100%', justifyContent: 'flex-start' }}
|
||||||
|
>
|
||||||
|
🔍 {t('common.examine')}
|
||||||
|
</GameButton>
|
||||||
|
</GameDropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{entityModal.type === 'npcs' && location.npcs
|
||||||
|
.filter((npc: any) => npc.type !== 'enemy')
|
||||||
|
.map((npc: any, i: number) => (
|
||||||
|
<div key={i} className="entity-card npc-card grid-card"
|
||||||
|
onClick={() => { handleNpcClick(npc); setEntityModal(null); }}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{npc.image_path ? (
|
||||||
|
<img src={getAssetPath(npc.image_path)} alt={getTranslatedText(npc.name)} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
|
) : <span className="entity-icon" style={{ fontSize: '2.5rem' }}>🧑</span>}
|
||||||
|
<div className="grid-overlay"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{entityModal.type === 'items' && [...location.items]
|
||||||
|
.sort((a: any, b: any) => (a.id || 0) - (b.id || 0))
|
||||||
|
.map((item: any, i: number) => {
|
||||||
|
const itemId = `modal-item-${item.id}-${i}`;
|
||||||
|
return (
|
||||||
|
<div key={itemId} className="entity-card item-card grid-card"
|
||||||
|
onClick={(e) => handleDropdownClick(e, itemId)}
|
||||||
|
>
|
||||||
|
<GameTooltip content={<ItemTooltipContent item={item} />}>
|
||||||
|
<div className="grid-corpse-content">
|
||||||
|
{item.image_path ? (
|
||||||
|
<img src={getAssetPath(item.image_path)} alt={getTranslatedText(item.name)} className="ground-item-image"
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
||||||
|
) : <div style={{ fontSize: '2rem' }}>{item.emoji}</div>}
|
||||||
|
{item.quantity > 1 && <div className="grid-quantity">x{item.quantity}</div>}
|
||||||
|
</div>
|
||||||
|
</GameTooltip>
|
||||||
|
{activeDropdown === itemId && (
|
||||||
|
<GameDropdown isOpen={true} onClose={() => setActiveDropdown(null)} width="200px">
|
||||||
|
<div className="game-dropdown-header">{getTranslatedText(item.name)}</div>
|
||||||
|
<GameButton variant="success" size="sm"
|
||||||
|
onClick={() => { playSfx('/audio/sfx/pickup.wav'); onPickup(Number(item.id), 1); setActiveDropdown(null); setEntityModal(null); }}
|
||||||
|
style={{ width: '100%', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
✋ {t('common.pickUp')}
|
||||||
|
</GameButton>
|
||||||
|
</GameDropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{entityModal.type === 'players' && (location.other_players || []).map((player: any, i: number) => {
|
||||||
|
const playerId = `modal-player-${player.id}-${i}`;
|
||||||
|
return (
|
||||||
|
<div key={i} className="entity-card player-card grid-card"
|
||||||
|
onClick={(e) => handleDropdownClick(e, playerId)}
|
||||||
|
>
|
||||||
|
<div className="grid-corpse-content">
|
||||||
|
<div style={{ fontSize: '2.5rem' }}>🧍</div>
|
||||||
|
<div className="grid-quantity" style={{ top: '2px', right: '2px', bottom: 'auto', background: 'rgba(49, 130, 206, 0.8)', borderColor: 'rgba(99, 179, 237, 0.4)' }}>
|
||||||
|
Lv.{player.level}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{activeDropdown === playerId && (
|
||||||
|
<GameDropdown isOpen={true} onClose={() => setActiveDropdown(null)} width="180px">
|
||||||
|
<div className="game-dropdown-header">{player.name || player.username}</div>
|
||||||
|
{player.can_pvp ? (
|
||||||
|
<GameButton variant="danger" size="sm"
|
||||||
|
onClick={() => { onInitiatePvP(player.id); setActiveDropdown(null); setEntityModal(null); }}
|
||||||
|
style={{ width: '100%', justifyContent: 'flex-start' }}
|
||||||
|
>
|
||||||
|
⚔️ {t('game.attack')}
|
||||||
|
</GameButton>
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '0.5rem', fontSize: '0.8rem', color: '#a0aec0' }}>PvP Unavailable</div>
|
||||||
|
)}
|
||||||
|
</GameDropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</GameModal>
|
||||||
|
)}
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,13 +173,7 @@ function MovementControls({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="movement-controls">
|
<div className="movement-controls">
|
||||||
<h3 className={movementCooldown > 0 ? 'cooldown-active' : ''}>
|
|
||||||
{movementCooldown > 0 ? (
|
|
||||||
<span key="timer">⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })}</span>
|
|
||||||
) : (
|
|
||||||
<span key="title">{t('game.travel')}</span>
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<div className="compass-grid">
|
<div className="compass-grid">
|
||||||
{/* Top row */}
|
{/* Top row */}
|
||||||
{renderCompassButton('northwest', '↖️', 'nw')}
|
{renderCompassButton('northwest', '↖️', 'nw')}
|
||||||
@@ -197,9 +191,8 @@ function MovementControls({
|
|||||||
{renderCompassButton('southeast', '↘️', 'se')}
|
{renderCompassButton('southeast', '↘️', 'se')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Special movements (Vertical only now, since Enter/Exit are in center) */}
|
|
||||||
{(location.directions.includes('up') || location.directions.includes('down')) && (
|
{(location.directions.includes('up') || location.directions.includes('down')) && (
|
||||||
<div className="special-moves">
|
<div className="special-moves" style={{ display: 'flex', justifyContent: 'center', gap: '0.5rem' }}>
|
||||||
{location.directions.includes('up') && (
|
{location.directions.includes('up') && (
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||||
<div className="movement-tooltip">
|
<div className="movement-tooltip">
|
||||||
@@ -209,8 +202,9 @@ function MovementControls({
|
|||||||
)}>
|
)}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onMove('up')}
|
onClick={() => onMove('up')}
|
||||||
className="special-btn"
|
className="compass-center-btn"
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
disabled={!!combatState || movementCooldown > 0}
|
||||||
|
style={{ padding: '0.3rem 1rem' }}
|
||||||
>
|
>
|
||||||
⬆️ {t('directions.up')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('up')}`}</span>
|
⬆️ {t('directions.up')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('up')}`}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -225,8 +219,9 @@ function MovementControls({
|
|||||||
)}>
|
)}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onMove('down')}
|
onClick={() => onMove('down')}
|
||||||
className="special-btn"
|
className="compass-center-btn"
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
disabled={!!combatState || movementCooldown > 0}
|
||||||
|
style={{ padding: '0.3rem 1rem' }}
|
||||||
>
|
>
|
||||||
⬇️ {t('directions.down')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('down')}`}</span>
|
⬇️ {t('directions.down')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('down')}`}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -239,7 +234,6 @@ function MovementControls({
|
|||||||
{/* Surroundings - outside movement controls */}
|
{/* Surroundings - outside movement controls */}
|
||||||
{location.interactables && location.interactables.length > 0 && (
|
{location.interactables && location.interactables.length > 0 && (
|
||||||
<div className="interactables-section">
|
<div className="interactables-section">
|
||||||
<h3>{t('game.surroundings')}</h3>
|
|
||||||
{location.interactables.map((interactable: any) => (
|
{location.interactables.map((interactable: any) => (
|
||||||
<div key={interactable.instance_id} className="interactable-card">
|
<div key={interactable.instance_id} className="interactable-card">
|
||||||
{interactable.image_path && (
|
{interactable.image_path && (
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ function PlayerSidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="sidebar-divider"></div>
|
|
||||||
|
|
||||||
{/* Compact 2x2 Stats Grid */}
|
{/* Compact 2x2 Stats Grid */}
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
@@ -263,10 +263,11 @@ function PlayerSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sidebar-divider"></div>
|
|
||||||
|
|
||||||
{/* Inventory Capacity - matching HP/Stamina/XP style */}
|
|
||||||
<div className="sidebar-stat-bar">
|
{/* Inventory Capacity - Weight & Volume side-by-side */}
|
||||||
|
<div className="sidebar-capacity-row">
|
||||||
|
<div className="sidebar-stat-bar" style={{ flex: 1 }}>
|
||||||
<GameProgressBar
|
<GameProgressBar
|
||||||
value={profile.current_weight || 0}
|
value={profile.current_weight || 0}
|
||||||
max={profile.max_weight || 0}
|
max={profile.max_weight || 0}
|
||||||
@@ -277,8 +278,7 @@ function PlayerSidebar({
|
|||||||
height="10px"
|
height="10px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sidebar-stat-bar" style={{ flex: 1 }}>
|
||||||
<div className="sidebar-stat-bar">
|
|
||||||
<GameProgressBar
|
<GameProgressBar
|
||||||
value={profile.current_volume || 0}
|
value={profile.current_volume || 0}
|
||||||
max={profile.max_volume || 0}
|
max={profile.max_volume || 0}
|
||||||
@@ -290,25 +290,26 @@ function PlayerSidebar({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="sidebar-buttons" style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginTop: '1rem' }}>
|
<div className="sidebar-buttons" style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||||||
<GameButton
|
<GameButton
|
||||||
className="open-inventory-btn"
|
className="open-inventory-btn"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowInventory(true)}
|
onClick={() => setShowInventory(true)}
|
||||||
style={{ width: '100%', justifyContent: 'center' }}
|
style={{ flex: 1, justifyContent: 'center' }}
|
||||||
>
|
>
|
||||||
{t('game.inventory')}
|
{t('game.inventory')}
|
||||||
</GameButton>
|
</GameButton>
|
||||||
|
|
||||||
<GameButton
|
<GameButton
|
||||||
className="quest-journal-btn"
|
className="quest-journal-btn"
|
||||||
variant="secondary" // Different color as requested
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onOpenQuestJournal}
|
onClick={onOpenQuestJournal}
|
||||||
style={{ width: '100%', justifyContent: 'center' }}
|
style={{ flex: 1, justifyContent: 'center' }}
|
||||||
>
|
>
|
||||||
📜 {t('common.quests')}
|
📜 {t('common.quests')}
|
||||||
</GameButton>
|
</GameButton>
|
||||||
|
|||||||
@@ -26,6 +26,19 @@
|
|||||||
--game-clip-path-sm: polygon(4px 0, 100% 0, 100% calc(100% - 4px), calc(100% - 4px) 100%, 0 100%, 0 4px);
|
--game-clip-path-sm: polygon(4px 0, 100% 0, 100% calc(100% - 4px), calc(100% - 4px) 100%, 0 100%, 0 4px);
|
||||||
--game-clip-path-no-br: polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px);
|
--game-clip-path-no-br: polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px);
|
||||||
|
|
||||||
|
/* --- Gaps and paddings --- */
|
||||||
|
--game-padding-xs: 0.25rem;
|
||||||
|
--game-padding-sm: 0.5rem;
|
||||||
|
--game-padding-md: 0.75rem;
|
||||||
|
--game-padding-lg: 1rem;
|
||||||
|
--game-padding-xl: 1.5rem;
|
||||||
|
|
||||||
|
--game-gap-xs: 0.25rem;
|
||||||
|
--game-gap-sm: 0.5rem;
|
||||||
|
--game-gap-md: 0.75rem;
|
||||||
|
--game-gap-lg: 1rem;
|
||||||
|
--game-gap-xl: 1.5rem;
|
||||||
|
|
||||||
/* --- Typography --- */
|
/* --- Typography --- */
|
||||||
--game-font-main: 'Saira Condensed', system-ui, sans-serif;
|
--game-font-main: 'Saira Condensed', system-ui, sans-serif;
|
||||||
--game-text-primary: #e0e0e0;
|
--game-text-primary: #e0e0e0;
|
||||||
@@ -68,6 +81,24 @@
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
/* Responsive Location Widths */
|
||||||
|
--location-content-width: 600px;
|
||||||
|
/* Default (1080p and below) */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2200px) {
|
||||||
|
:root {
|
||||||
|
--location-content-width: 1000px;
|
||||||
|
/* 1440p */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 3400px) {
|
||||||
|
:root {
|
||||||
|
--location-content-width: 1400px;
|
||||||
|
/* 4K */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Reusable Game Classes --- */
|
/* --- Reusable Game Classes --- */
|
||||||
|
|||||||