diff --git a/pwa/src/App.tsx b/pwa/src/App.tsx
index a0fd283..05444b7 100644
--- a/pwa/src/App.tsx
+++ b/pwa/src/App.tsx
@@ -1,4 +1,4 @@
-import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
+import { BrowserRouter, HashRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import { useAuth } from './hooks/useAuth'
import LandingPage from './components/LandingPage'
@@ -13,6 +13,10 @@ import GameLayout from './components/GameLayout'
import AccountPage from './components/AccountPage'
import './App.css'
+// Use HashRouter for Electron (file:// protocol), BrowserRouter for web
+const isElectron = window.location.protocol === 'file:'
+const Router = isElectron ? HashRouter : BrowserRouter
+
function PrivateRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, loading } = useAuth()
diff --git a/pwa/src/components/Game.tsx b/pwa/src/components/Game.tsx
index 748efcd..674516b 100644
--- a/pwa/src/components/Game.tsx
+++ b/pwa/src/components/Game.tsx
@@ -289,7 +289,6 @@ function Game() {
{/* Combat view (when in combat) */}
{state.combatState && state.playerState && (
- console.log('Rendering Combat component', state.combatState),
void
- onFlee: () => void
- onPvPAction: (action: string) => void
- onExitCombat: () => void
- onExitPvPCombat: () => void
- flashEnemy?: boolean
- buttonsDisabled?: boolean
- floatingTexts?: { id: number, text: string, x: number, y: number, type: 'damage-player' | 'damage-enemy' | 'damage-player-dealt' | 'heal' }[]
-}
-
-function CombatView({
- combatState,
- combatLog,
- profile: _profile,
- playerState,
- enemyName,
- enemyImage,
- enemyTurnMessage,
- pvpTimeRemaining,
- turnTimeRemaining,
- onCombatAction,
- onPvPAction,
- onExitCombat,
- onExitPvPCombat,
- flashEnemy,
- buttonsDisabled,
- floatingTexts = []
-}: CombatViewProps) {
- return (
-
-
-
- {combatState.is_pvp ? '⚔️ PvP Combat' : `⚔️ Combat - ${enemyName || combatState.combat?.npc_name || 'Enemy'}`}
-
-
-
- {combatState.is_pvp ? (
- /* PvP Combat UI - Unified Layout */
-
-
- {/* Opponent Display (using same structure as PvE Enemy) */}
-
- {floatingTexts.map(ft => (
-
- {ft.text}
-
- ))}
- {(() => {
- if (!combatState.pvp_combat) return null
- const opponent = combatState.pvp_combat.is_attacker ?
- combatState.pvp_combat.defender :
- combatState.pvp_combat.attacker
-
- if (!opponent) return
❓
- // Use a default avatar if no image, or maybe the class image if available?
- // For now, let's use a placeholder or try to get it from profile if passed?
- // The opponent object has: username, level, hp, max_hp.
- // It might not have an image url.
- return (
-
- 👤
-
{opponent.username} (Lv. {opponent.level})
-
- )
- })()}
-
-
-
- {/* Opponent HP Bar */}
- {(() => {
- if (!combatState.pvp_combat) return null
- const opponent = combatState.pvp_combat.is_attacker ?
- combatState.pvp_combat.defender :
- combatState.pvp_combat.attacker
-
- if (!opponent) return null
-
- return (
-
-
-
- {opponent.username}: {opponent.hp} / {opponent.max_hp}
-
-
-
-
- )
- })()}
-
- {/* Player HP Bar */}
- {(() => {
- if (!combatState.pvp_combat) return null
- const you = combatState.pvp_combat.is_attacker ?
- combatState.pvp_combat.attacker :
- combatState.pvp_combat.defender
-
- if (!you) return null
-
- return (
-
-
-
- You: {you.hp} / {you.max_hp}
-
-
-
-
- )
- })()}
-
-
-
-
- {combatState.pvp_combat.combat_over ? (
-
- {combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? "🏃 Combat Ended" : "💀 Combat Over"}
-
- ) : combatState.pvp_combat.your_turn ? (
- ✅ Your Turn ({pvpTimeRemaining ?? combatState.pvp_combat.time_remaining}s)
- ) : (
- ⏳ Opponent's Turn ({pvpTimeRemaining ?? combatState.pvp_combat.time_remaining}s)
- )}
-
-
-
- {!combatState.pvp_combat.combat_over ? (
- <>
- onPvPAction('attack')}
- disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
- >
- ⚔️ Attack
-
- onPvPAction('flee')}
- disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
- >
- 🏃 Flee
-
- >
- ) : (
-
- {combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? '✅ Continue' : '💀 Return'}
-
- )}
-
-
- {/* Combat Log */}
-
-
Combat Log
-
-
- {combatLog.map((entry: any, i: number) => (
-
- [{entry.time}]
- {entry.message}
-
- ))}
- {combatLog.length === 0 &&
PvP Combat started...
}
-
-
-
-
- ) : (
- /* PvE Combat UI */
- <>
-
-
- {/* Intent Bubble - Moved here to avoid overflow:hidden clipping */}
- {combatState.combat?.npc_intent && !combatState.combat_over && (
-
-
- {combatState.combat.npc_intent === 'attack' ? '⚔️' :
- combatState.combat.npc_intent === 'defend' ? '🛡️' :
- combatState.combat.npc_intent === 'special' ? '🔥' : '❓'}
-
- {combatState.combat.npc_intent}
-
- )}
-
-
- {floatingTexts.map(ft => (
-
- {ft.text}
-
- ))}
-
-
-
-
-
-
- Enemy HP: {combatState.combat?.npc_hp || 0} / {combatState.combat?.npc_max_hp || 100}
-
-
-
-
- {playerState && (
-
-
-
- Your HP: {playerState.health} / {playerState.max_health}
-
-
-
-
- )}
-
-
-
-
- {!combatState.combat_over ? (
- enemyTurnMessage ? (
- 🗡️ Enemy's turn...
- ) : combatState.combat?.turn === 'player' ? (
- <>
- ✅ Your Turn
- {turnTimeRemaining !== null && (
-
- ⏱️ {Math.floor(turnTimeRemaining / 60)}:{String(Math.floor(turnTimeRemaining % 60)).padStart(2, '0')}
-
- )}
- >
- ) : (
- ⚠️ Enemy Turn
- )
- ) : (
-
- {combatState.player_won ? "✅ Victory!" : combatState.player_fled ? "🏃 Escaped!" : "💀 Defeated"}
-
- )}
-
-
- {/* PvE Combat Actions */}
-
-
- {!combatState.combat_over ? (
- <>
- onCombatAction('attack')}
- disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
- >
- ⚔️ Attack
-
- onCombatAction('flee')}
- disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
- >
- 🏃 Flee
-
- >
- ) : (
-
- {combatState.player_won ? '✅ Continue' : combatState.player_fled ? '✅ Continue' : '💀 Return'}
-
- )}
-
-
- {/* Combat Log */}
-
-
Combat Log
-
-
- {combatLog.map((entry: any, i: number) => (
-
- [{entry.time}]
- {entry.message}
-
- ))}
- {combatLog.length === 0 &&
Combat started...
}
-
-
-
-
- >
- )}
-
- )
-}
-
-export default CombatView
diff --git a/pwa/src/components/game/LocationView.tsx b/pwa/src/components/game/LocationView.tsx
index 826e178..18072f8 100644
--- a/pwa/src/components/game/LocationView.tsx
+++ b/pwa/src/components/game/LocationView.tsx
@@ -180,14 +180,14 @@ function LocationView({
{enemy.id && (
{ e.currentTarget.style.display = 'none' }}
/>
)}
-
{enemy.name}
+
{getTranslatedText(enemy.name)}
{enemy.level &&
Lv. {enemy.level}
}
-
{corpse.emoji} {corpse.name}
+
{corpse.emoji} {getTranslatedText(corpse.name)}
{corpse.loot_count} item(s)
- {item.emoji} {item.item_name}
+ {item.emoji} {getTranslatedText(item.item_name)}
Qty: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
{item.required_tool && (
- 🔧 {item.required_tool_name} {item.has_tool ? '✓' : '✗'}
+ 🔧 {getTranslatedText(item.required_tool_name)} {item.has_tool ? '✓' : '✗'}
)}
@@ -256,7 +256,7 @@ function LocationView({
className="corpse-item-loot-btn"
onClick={() => onLootCorpseItem(String(corpse.id), item.index)}
disabled={!item.can_loot}
- title={!item.can_loot ? `Requires ${item.required_tool_name}` : 'Loot this item'}
+ title={!item.can_loot ? `Requires ${getTranslatedText(item.required_tool_name)}` : 'Loot this item'}
>
{item.can_loot ? '📦 Loot' : '🔒'}
@@ -286,7 +286,7 @@ function LocationView({
🧑
-
{npc.name}
+
{getTranslatedText(npc.name)}
{npc.level &&
Lv. {npc.level}
}
Talk
@@ -306,7 +306,7 @@ function LocationView({
{item.image_path ? (
{
(e.target as HTMLImageElement).style.display = 'none';
@@ -318,14 +318,14 @@ function LocationView({
{item.emoji || '📦'}
- {item.name || 'Unknown Item'}
+ {getTranslatedText(item.name) || 'Unknown Item'}
{item.quantity > 1 &&
×{item.quantity}
}
Info
- {item.description &&
{item.description}
}
+ {item.description &&
{getTranslatedText(item.description)}
}
{item.weight !== undefined && item.weight > 0 && (
⚖️ Weight: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
diff --git a/pwa/src/utils/assetPath.ts b/pwa/src/utils/assetPath.ts
index f3ef615..6376f1e 100644
--- a/pwa/src/utils/assetPath.ts
+++ b/pwa/src/utils/assetPath.ts
@@ -2,12 +2,15 @@
* Asset Path Utility
*
* Resolves asset paths based on runtime environment:
- * - Electron: Returns local path (assets bundled with app)
+ * - Electron: Returns relative path (assets bundled with app, using ./)
* - Browser: Returns full server URL
*/
-// Detect if running in Electron
-const isElectron = !!(window as any).electronAPI?.isElectron
+// Detect if running in Electron - check at runtime, not module load time
+function checkIsElectron(): boolean {
+ return !!(window as any).electronAPI?.isElectron ||
+ window.location.protocol === 'file:'
+}
// Base URL for remote assets (browser mode)
const ASSET_BASE_URL = import.meta.env.VITE_ASSET_URL ||
@@ -21,21 +24,21 @@ const ASSET_BASE_URL = import.meta.env.VITE_ASSET_URL ||
export function getAssetPath(path: string): string {
if (!path) return ''
- // Normalize path (ensure leading slash)
- const normalizedPath = path.startsWith('/') ? path : `/${path}`
+ // Normalize path (remove leading slash for Electron compatibility)
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path
- if (isElectron) {
- // In Electron, assets are served relative to the app
- return normalizedPath
+ if (checkIsElectron()) {
+ // In Electron with base: './', use relative paths
+ return `./${cleanPath}`
}
// In browser, prepend the server URL
- return `${ASSET_BASE_URL}${normalizedPath}`
+ return `${ASSET_BASE_URL}/${cleanPath}`
}
/**
* Check if we're running in Electron
*/
export function isElectronApp(): boolean {
- return isElectron
+ return checkIsElectron()
}