diff --git a/pwa/src/components/game/LocationView.css b/pwa/src/components/game/LocationView.css new file mode 100644 index 0000000..dd0712e --- /dev/null +++ b/pwa/src/components/game/LocationView.css @@ -0,0 +1,119 @@ +/* Grid View Styles */ +.entity-list.grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 1rem; + align-items: start; +} + +.entity-card.grid-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + aspect-ratio: 1; + padding: 0.5rem; + text-align: center; + background: rgba(0, 0, 0, 0.4); + border: 1px solid var(--game-border-color); + border-radius: 8px; + /* Slightly more rounded for grid items */ + transition: all 0.2s; + cursor: pointer; + position: relative; + overflow: hidden; + height: 100%; +} + +.entity-card.grid-card:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border-color: var(--game-text-highlight); +} + +.grid-card .entity-image { + width: 60%; + height: 60%; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.grid-card .entity-image img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.grid-card .grid-quantity { + position: absolute; + bottom: 4px; + right: 4px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 0.75rem; + padding: 1px 4px; + border-radius: 4px; + font-weight: bold; +} + +.grid-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; +} + +.grid-corpse-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + justify-content: center; + height: 100%; +} + +/* Specific Border Colors for Grid Cards to match List View */ +.enemy-card.grid-card { + border-color: rgba(244, 67, 54, 0.5); +} + +.enemy-card.grid-card:hover { + border-color: rgba(244, 67, 54, 1); + background: rgba(244, 67, 54, 0.1); +} + +.corpse-card.grid-card { + border-color: rgba(156, 39, 176, 0.5); +} + +.corpse-card.grid-card:hover { + border-color: rgba(156, 39, 176, 1); + background: rgba(156, 39, 176, 0.1); +} + +.npc-card.grid-card { + border-color: rgba(107, 185, 240, 0.5); +} + +.npc-card.grid-card:hover { + border-color: rgba(107, 185, 240, 1); + background: rgba(107, 185, 240, 0.1); +} + +.item-card.grid-card { + border-color: rgba(76, 175, 80, 0.5); +} + +.item-card.grid-card:hover { + border-color: rgba(76, 175, 80, 1); + background: rgba(76, 175, 80, 0.1); +} + +.view-toggle-btn { + border: 1px solid var(--game-border-color); +} \ No newline at end of file diff --git a/pwa/src/components/game/LocationView.tsx b/pwa/src/components/game/LocationView.tsx index 5d26a40..6a66bf1 100644 --- a/pwa/src/components/game/LocationView.tsx +++ b/pwa/src/components/game/LocationView.tsx @@ -1,10 +1,14 @@ import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAudio } from '../../contexts/AudioContext' import Workbench from './Workbench' import { GameTooltip } from '../common/GameTooltip' +import { GameButton } from '../common/GameButton' +import { GameDropdown } from '../common/GameDropdown' import { getAssetPath } from '../../utils/assetPath' import { getTranslatedText } from '../../utils/i18nUtils' +import './LocationView.css' interface LocationViewProps { location: Location @@ -88,6 +92,44 @@ function LocationView({ const { t } = useTranslation() const { playSfx } = useAudio() + // View Mode State + const [viewMode, setViewMode] = useState<'list' | 'grid'>(() => { + return (localStorage.getItem('locationViewMode') as 'list' | 'grid') || 'list' + }) + + // Dropdown State + const [activeDropdown, setActiveDropdown] = useState(null) + const [dropdownPos, setDropdownPos] = useState({ x: 0, y: 0 }) + + const toggleViewMode = () => { + const newMode = viewMode === 'list' ? 'grid' : 'list' + setViewMode(newMode) + localStorage.setItem('locationViewMode', newMode) + playSfx('/audio/sfx/click.wav') + } + + // Handle dropdown toggle + const handleDropdownClick = (e: React.MouseEvent, id: string) => { + e.stopPropagation() + if (activeDropdown === id) { + setActiveDropdown(null) + } else { + const rect = e.currentTarget.getBoundingClientRect() + setDropdownPos({ + x: rect.left + window.scrollX, + y: rect.bottom + window.scrollY + 5 + }) + setActiveDropdown(id) + } + } + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = () => setActiveDropdown(null) + window.addEventListener('click', handleClickOutside) + return () => window.removeEventListener('click', handleClickOutside) + }, []) + return (
@@ -105,6 +147,15 @@ function LocationView({ )} + { e.stopPropagation(); toggleViewMode(); }} + className="view-toggle-btn" + style={{ marginLeft: 'auto' }} + > + {viewMode === 'list' ? '📋' : '🔲'} + {location.tags && location.tags.length > 0 && ( @@ -179,34 +230,82 @@ function LocationView({ )}
+ {/* Enemies */} {/* Enemies */} {location.npcs.filter((npc: any) => npc.type === 'enemy').length > 0 && (

{t('location.enemies')}

-
- {location.npcs.filter((npc: any) => npc.type === 'enemy').map((enemy: any, i: number) => ( -
- {enemy.id && ( -
- {getTranslatedText(enemy.name)} { e.currentTarget.style.display = 'none' }} - /> -
- )} -
-
{getTranslatedText(enemy.name)}
- {enemy.level &&
{t('location.level')} {enemy.level}
} +
+ {location.npcs.filter((npc: any) => npc.type === 'enemy').map((enemy: any, i: number) => { + const id = `enemy-${enemy.id || i}`; + return ( +
handleDropdownClick(e, id) : undefined}> + {enemy.id && ( +
+ {getTranslatedText(enemy.name)} { e.currentTarget.style.display = 'none' }} + /> +
+ )} + + {viewMode === 'list' && ( + <> +
+
{getTranslatedText(enemy.name)}
+ {enemy.level &&
{t('location.level')} {enemy.level}
} +
+ onInitiateCombat(enemy.id)} + > + {t('common.fight')} + + + )} + + {viewMode === 'grid' && ( + +
{getTranslatedText(enemy.name)}
+
{t('location.level')} {enemy.level}
+
Click for actions
+
+ }> +
+ + )} + + {/* Dropdown for Grid View */} + {viewMode === 'grid' && activeDropdown === id && ( + setActiveDropdown(null)} + position={dropdownPos} + width="160px" + > +
{getTranslatedText(enemy.name)}
+ { onInitiateCombat(enemy.id); setActiveDropdown(null); }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + ⚔️ {t('common.fight')} + +
+
+ {t('location.level')} {enemy.level} +
+ + )}
- -
- ))} + ); + })}
)} @@ -218,21 +317,65 @@ function LocationView({
{location.corpses.map((corpse: any) => (
-
-
-
{corpse.emoji} {getTranslatedText(corpse.name)}
-
{corpse.loot_count} {t('location.items')}
-
- +
handleDropdownClick(e, `corpse-${corpse.id}`) : undefined} + > + {viewMode === 'list' ? ( + <> +
+
{corpse.emoji} {getTranslatedText(corpse.name)}
+
{corpse.loot_count} {t('location.items')}
+
+ { + playSfx('/audio/sfx/interact.wav') + onLootCorpse(String(corpse.id)) + }} + disabled={corpse.loot_count === 0} + > + 🔍 {t('common.examine')} + + + ) : ( + +
{corpse.emoji} {getTranslatedText(corpse.name)}
+
{corpse.loot_count} {t('location.items')}
+
+ }> +
+
{corpse.emoji}
+
{corpse.loot_count} items
+
+ + )} + + {viewMode === 'grid' && activeDropdown === `corpse-${corpse.id}` && ( + setActiveDropdown(null)} + position={dropdownPos} + width="160px" + > +
{getTranslatedText(corpse.name)}
+ { + playSfx('/audio/sfx/interact.wav') + onLootCorpse(String(corpse.id)) + setActiveDropdown(null) + }} + disabled={corpse.loot_count === 0} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + 🔍 {t('common.examine')} + +
+ )}
{expandedCorpse === String(corpse.id) && corpseDetails && corpseDetails.loot_items && ( @@ -296,13 +439,42 @@ function LocationView({

{t('location.npcs')}

{location.npcs.filter((npc: any) => npc.type !== 'enemy').map((npc: any, i: number) => ( -
- 🧑 -
-
{getTranslatedText(npc.name)}
- {npc.level &&
{t('location.level')} {npc.level}
} -
- +
handleDropdownClick(e, `npc-${i}`) : undefined} + > + 🧑 + + {viewMode === 'list' ? ( + <> +
+
{getTranslatedText(npc.name)}
+ {npc.level &&
{t('location.level')} {npc.level}
} +
+ {t('common.talk')} + + ) : ( + +
{getTranslatedText(npc.name)}
+
{t('location.level')} {npc.level}
+
+ }> +
+ + )} + + {viewMode === 'grid' && activeDropdown === `npc-${i}` && ( + setActiveDropdown(null)} + position={dropdownPos} + > +
{getTranslatedText(npc.name)}
+ + 💬 {t('common.talk')} + +
+ )}
))}
@@ -317,8 +489,11 @@ function LocationView({ {location.items.map((item: any, i: number) => { // Use loose equality to handle potential string/number mismatches const isShaking = failedActionItemId == item.id; + const itemId = `item-${item.id}-${i}`; return ( -
+
handleDropdownClick(e, itemId) : undefined} + > {item.description &&
{getTranslatedText(item.description)}
} @@ -353,12 +528,13 @@ function LocationView({ )}
}> -
+
{item.image_path ? ( {getTranslatedText(item.name)} { (e.target as HTMLImageElement).style.display = 'none'; const icon = (e.target as HTMLImageElement).nextElementSibling; @@ -366,59 +542,98 @@ function LocationView({ }} /> ) : null} - {item.emoji || '📦'} -
-
- {getTranslatedText(item.name) || 'Unknown Item'} + {item.emoji || '📦'} + + {viewMode === 'list' && ( +
+
+ {getTranslatedText(item.name) || 'Unknown Item'} +
+ {item.quantity > 1 &&
×{item.quantity}
}
- {item.quantity > 1 &&
×{item.quantity}
} -
+ )} + {viewMode === 'grid' && item.quantity > 1 && ( +
x{item.quantity}
+ )}
-
- {item.quantity === 1 ? ( - - ) : ( -
- - - {item.quantity >= 5 && ( - - )} - - {item.quantity >= 10 && ( - - )} - - -
- )} -
+ 🤚 {t('common.pickUp')} (x1) + + {item.quantity > 1 && ( + { onPickup(item.id, item.quantity); setActiveDropdown(null); }} + style={{ width: '100%', justifyContent: 'flex-start' }} + > + 🤚 {t('common.pickUp')} ({t('common.all')}) + + )} + + )}
); })} diff --git a/pwa/src/components/game/PlayerSidebar.tsx b/pwa/src/components/game/PlayerSidebar.tsx index 2bc02f2..6cae566 100644 --- a/pwa/src/components/game/PlayerSidebar.tsx +++ b/pwa/src/components/game/PlayerSidebar.tsx @@ -6,6 +6,7 @@ import { getTranslatedText } from '../../utils/i18nUtils' import InventoryModal from './InventoryModal' import { GameProgressBar } from '../common/GameProgressBar' import { GameTooltip } from '../common/GameTooltip' +import { GameButton } from '../common/GameButton' import { useAudio } from '../../contexts/AudioContext' interface PlayerSidebarProps { @@ -289,12 +290,15 @@ function PlayerSidebar({
)} - +