WIP: i18n implementation - fix items.json syntax, add useTranslation hooks to components
This commit is contained in:
@@ -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 { AuthProvider } from './contexts/AuthContext'
|
||||||
import { useAuth } from './hooks/useAuth'
|
import { useAuth } from './hooks/useAuth'
|
||||||
import LandingPage from './components/LandingPage'
|
import LandingPage from './components/LandingPage'
|
||||||
@@ -13,6 +13,10 @@ import GameLayout from './components/GameLayout'
|
|||||||
import AccountPage from './components/AccountPage'
|
import AccountPage from './components/AccountPage'
|
||||||
import './App.css'
|
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 }) {
|
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, loading } = useAuth()
|
const { isAuthenticated, loading } = useAuth()
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ function Game() {
|
|||||||
<div className="center-area">
|
<div className="center-area">
|
||||||
{/* Combat view (when in combat) */}
|
{/* Combat view (when in combat) */}
|
||||||
{state.combatState && state.playerState && (
|
{state.combatState && state.playerState && (
|
||||||
console.log('Rendering Combat component', state.combatState),
|
|
||||||
<Combat
|
<Combat
|
||||||
combatState={state.combatState}
|
combatState={state.combatState}
|
||||||
combatLog={state.combatLog}
|
combatLog={state.combatLog}
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
import type { CombatState, CombatLogEntry, Profile, Equipment, PlayerState } from './types'
|
|
||||||
|
|
||||||
interface CombatViewProps {
|
|
||||||
combatState: CombatState
|
|
||||||
combatLog: CombatLogEntry[]
|
|
||||||
profile: Profile | null
|
|
||||||
playerState: PlayerState | null
|
|
||||||
equipment: Equipment
|
|
||||||
enemyName: string
|
|
||||||
enemyImage: string
|
|
||||||
enemyTurnMessage: string
|
|
||||||
pvpTimeRemaining: number | null
|
|
||||||
turnTimeRemaining: number | null
|
|
||||||
onCombatAction: (action: string) => 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 (
|
|
||||||
<div className="combat-view">
|
|
||||||
<div className="combat-header-inline">
|
|
||||||
<h2>
|
|
||||||
{combatState.is_pvp ? '⚔️ PvP Combat' : `⚔️ Combat - ${enemyName || combatState.combat?.npc_name || 'Enemy'}`}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{combatState.is_pvp ? (
|
|
||||||
/* PvP Combat UI - Unified Layout */
|
|
||||||
<div className="combat-content-wrapper">
|
|
||||||
<div className="combat-enemy-display-inline">
|
|
||||||
{/* Opponent Display (using same structure as PvE Enemy) */}
|
|
||||||
<div className="combat-enemy-image-large">
|
|
||||||
{floatingTexts.map(ft => (
|
|
||||||
<div
|
|
||||||
key={ft.id}
|
|
||||||
className={`floating-text ${ft.type}`}
|
|
||||||
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}>
|
|
||||||
{ft.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{(() => {
|
|
||||||
if (!combatState.pvp_combat) return null
|
|
||||||
const opponent = combatState.pvp_combat.is_attacker ?
|
|
||||||
combatState.pvp_combat.defender :
|
|
||||||
combatState.pvp_combat.attacker
|
|
||||||
|
|
||||||
if (!opponent) return <div className="pvp-opponent-avatar">❓</div>
|
|
||||||
// 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 (
|
|
||||||
<div className="pvp-opponent-avatar" style={{ fontSize: '4rem', textAlign: 'center' }}>
|
|
||||||
👤
|
|
||||||
<div style={{ fontSize: '1rem', marginTop: '0.5rem' }}>{opponent.username} (Lv. {opponent.level})</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="combat-enemy-info-inline">
|
|
||||||
{/* 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 (
|
|
||||||
<div className="combat-hp-bar-container-inline enemy-hp-bar">
|
|
||||||
<div className="combat-hp-bar-inline">
|
|
||||||
<div className="combat-stat-label-inline">
|
|
||||||
{opponent.username}: {opponent.hp} / {opponent.max_hp}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="combat-hp-fill-inline"
|
|
||||||
style={{
|
|
||||||
width: `${(opponent.hp / opponent.max_hp) * 100}%`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* 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 (
|
|
||||||
<div className="combat-hp-bar-container-inline player-hp-bar" style={{ marginTop: '0.75rem' }}>
|
|
||||||
<div className="combat-hp-bar-inline" style={{ background: 'rgba(255, 107, 107, 0.2)' }}>
|
|
||||||
<div className="combat-stat-label-inline" style={{ color: '#ff6b6b' }}>
|
|
||||||
You: {you.hp} / {you.max_hp}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="combat-hp-fill-inline"
|
|
||||||
style={{
|
|
||||||
width: `${(you.hp / you.max_hp) * 100}%`,
|
|
||||||
background: 'linear-gradient(90deg, #f44336, #ff6b6b)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="combat-turn-indicator-inline">
|
|
||||||
{combatState.pvp_combat.combat_over ? (
|
|
||||||
<span className={combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? "your-turn" : "enemy-turn"}>
|
|
||||||
{combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? "🏃 Combat Ended" : "💀 Combat Over"}
|
|
||||||
</span>
|
|
||||||
) : combatState.pvp_combat.your_turn ? (
|
|
||||||
<span className="your-turn">✅ Your Turn ({pvpTimeRemaining ?? combatState.pvp_combat.time_remaining}s)</span>
|
|
||||||
) : (
|
|
||||||
<span className="enemy-turn">⏳ Opponent's Turn ({pvpTimeRemaining ?? combatState.pvp_combat.time_remaining}s)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="combat-actions-inline">
|
|
||||||
{!combatState.pvp_combat.combat_over ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="combat-action-btn attack-btn"
|
|
||||||
onClick={() => onPvPAction('attack')}
|
|
||||||
disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
|
|
||||||
>
|
|
||||||
⚔️ Attack
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="combat-action-btn flee-btn"
|
|
||||||
onClick={() => onPvPAction('flee')}
|
|
||||||
disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
|
|
||||||
>
|
|
||||||
🏃 Flee
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="combat-action-btn exit-btn"
|
|
||||||
onClick={onExitPvPCombat}
|
|
||||||
>
|
|
||||||
{combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? '✅ Continue' : '💀 Return'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Combat Log */}
|
|
||||||
<div className="combat-log-wrapper">
|
|
||||||
<h3 className="combat-log-title">Combat Log</h3>
|
|
||||||
<div className="combat-log-inline">
|
|
||||||
<div className="log-entries">
|
|
||||||
{combatLog.map((entry: any, i: number) => (
|
|
||||||
<div key={i} className={`log-entry ${entry.isPlayer ? 'player-log' : 'enemy-log'}`}>
|
|
||||||
<span className="log-time">[{entry.time}]</span>
|
|
||||||
<span className="log-message">{entry.message}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{combatLog.length === 0 && <div className="log-entry"><span className="log-message">PvP Combat started...</span></div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* PvE Combat UI */
|
|
||||||
<>
|
|
||||||
<div className="combat-content-wrapper">
|
|
||||||
<div className="combat-enemy-display-inline">
|
|
||||||
{/* Intent Bubble - Moved here to avoid overflow:hidden clipping */}
|
|
||||||
{combatState.combat?.npc_intent && !combatState.combat_over && (
|
|
||||||
<div className={`intent-bubble intent-${combatState.combat.npc_intent}`}>
|
|
||||||
<span className="intent-icon">
|
|
||||||
{combatState.combat.npc_intent === 'attack' ? '⚔️' :
|
|
||||||
combatState.combat.npc_intent === 'defend' ? '🛡️' :
|
|
||||||
combatState.combat.npc_intent === 'special' ? '🔥' : '❓'}
|
|
||||||
</span>
|
|
||||||
<span className="intent-desc">{combatState.combat.npc_intent}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="combat-enemy-image-large">
|
|
||||||
{floatingTexts.map(ft => (
|
|
||||||
<div
|
|
||||||
key={ft.id}
|
|
||||||
className={`floating-text ${ft.type}`}
|
|
||||||
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}>
|
|
||||||
{ft.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<img
|
|
||||||
src={enemyImage || combatState.combat?.npc_image || combatState.combat_image}
|
|
||||||
alt={enemyName || combatState.combat?.npc_name || 'Enemy'}
|
|
||||||
className={`${flashEnemy ? 'flash-hit' : ''
|
|
||||||
} ${combatState.combat_over && combatState.player_won ? 'enemy-dead' : ''
|
|
||||||
} ${combatState.combat_over && combatState.player_fled ? 'enemy-fled' : ''
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="combat-enemy-info-inline">
|
|
||||||
<div className="combat-hp-bar-container-inline enemy-hp-bar">
|
|
||||||
<div className="combat-hp-bar-inline">
|
|
||||||
<div className="combat-stat-label-inline">
|
|
||||||
Enemy HP: {combatState.combat?.npc_hp || 0} / {combatState.combat?.npc_max_hp || 100}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="combat-hp-fill-inline"
|
|
||||||
style={{
|
|
||||||
width: `${((combatState.combat?.npc_hp || 0) / (combatState.combat?.npc_max_hp || 100)) * 100}%`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{playerState && (
|
|
||||||
<div className="combat-hp-bar-container-inline player-hp-bar" style={{ marginTop: '0.75rem' }}>
|
|
||||||
<div className="combat-hp-bar-inline" style={{ background: 'rgba(255, 107, 107, 0.2)' }}>
|
|
||||||
<div className="combat-stat-label-inline" style={{ color: '#ff6b6b' }}>
|
|
||||||
Your HP: {playerState.health} / {playerState.max_health}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="combat-hp-fill-inline"
|
|
||||||
style={{
|
|
||||||
width: `${(playerState.health / playerState.max_health) * 100}%`,
|
|
||||||
background: 'linear-gradient(90deg, #f44336, #ff6b6b)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`combat-turn-indicator-inline ${enemyTurnMessage ? 'enemy-turn-message' : ''}`}>
|
|
||||||
{!combatState.combat_over ? (
|
|
||||||
enemyTurnMessage ? (
|
|
||||||
<span className="enemy-turn">🗡️ Enemy's turn...</span>
|
|
||||||
) : combatState.combat?.turn === 'player' ? (
|
|
||||||
<>
|
|
||||||
<span className="your-turn">✅ Your Turn</span>
|
|
||||||
{turnTimeRemaining !== null && (
|
|
||||||
<span className="turn-timer" style={{ marginLeft: '1rem', fontSize: '0.9em', opacity: 0.8 }}>
|
|
||||||
⏱️ {Math.floor(turnTimeRemaining / 60)}:{String(Math.floor(turnTimeRemaining % 60)).padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="enemy-turn">⚠️ Enemy Turn</span>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className={combatState.player_won ? "your-turn" : combatState.player_fled ? "your-turn" : "enemy-turn"}>
|
|
||||||
{combatState.player_won ? "✅ Victory!" : combatState.player_fled ? "🏃 Escaped!" : "💀 Defeated"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PvE Combat Actions */}
|
|
||||||
|
|
||||||
<div className="combat-actions-inline">
|
|
||||||
{!combatState.combat_over ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="combat-action-btn attack-btn"
|
|
||||||
onClick={() => onCombatAction('attack')}
|
|
||||||
disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
|
|
||||||
>
|
|
||||||
⚔️ Attack
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="combat-action-btn flee-btn"
|
|
||||||
onClick={() => onCombatAction('flee')}
|
|
||||||
disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
|
|
||||||
>
|
|
||||||
🏃 Flee
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="combat-action-btn exit-btn"
|
|
||||||
onClick={onExitCombat}
|
|
||||||
>
|
|
||||||
{combatState.player_won ? '✅ Continue' : combatState.player_fled ? '✅ Continue' : '💀 Return'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Combat Log */}
|
|
||||||
<div className="combat-log-wrapper">
|
|
||||||
<h3 className="combat-log-title">Combat Log</h3>
|
|
||||||
<div className="combat-log-inline">
|
|
||||||
<div className="log-entries">
|
|
||||||
{combatLog.map((entry: any, i: number) => (
|
|
||||||
<div key={i} className={`log-entry ${entry.isPlayer ? 'player-log' : 'enemy-log'}`}>
|
|
||||||
<span className="log-time">[{entry.time}]</span>
|
|
||||||
<span className="log-message">{entry.message}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{combatLog.length === 0 && <div className="log-entry"><span className="log-message">Combat started...</span></div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CombatView
|
|
||||||
@@ -180,14 +180,14 @@ function LocationView({
|
|||||||
{enemy.id && (
|
{enemy.id && (
|
||||||
<div className="entity-image">
|
<div className="entity-image">
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(enemy.image_path || `images/npcs/${enemy.name.toLowerCase().replace(/ /g, '_')}.webp`)}
|
src={getAssetPath(enemy.image_path || `images/npcs/${(typeof enemy.name === 'string' ? enemy.name : enemy.name?.en || '').toLowerCase().replace(/ /g, '_')}.webp`)}
|
||||||
alt={enemy.name}
|
alt={getTranslatedText(enemy.name)}
|
||||||
onError={(e: any) => { e.currentTarget.style.display = 'none' }}
|
onError={(e: any) => { e.currentTarget.style.display = 'none' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="entity-info">
|
<div className="entity-info">
|
||||||
<div className="entity-name enemy-name">{enemy.name}</div>
|
<div className="entity-name enemy-name">{getTranslatedText(enemy.name)}</div>
|
||||||
{enemy.level && <div className="entity-level">Lv. {enemy.level}</div>}
|
{enemy.level && <div className="entity-level">Lv. {enemy.level}</div>}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -211,7 +211,7 @@ function LocationView({
|
|||||||
<div key={corpse.id} className="corpse-container">
|
<div key={corpse.id} className="corpse-container">
|
||||||
<div className="entity-card corpse-card">
|
<div className="entity-card corpse-card">
|
||||||
<div className="entity-info">
|
<div className="entity-info">
|
||||||
<div className="entity-name">{corpse.emoji} {corpse.name}</div>
|
<div className="entity-name">{corpse.emoji} {getTranslatedText(corpse.name)}</div>
|
||||||
<div className="corpse-loot-count">{corpse.loot_count} item(s)</div>
|
<div className="corpse-loot-count">{corpse.loot_count} item(s)</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -241,14 +241,14 @@ function LocationView({
|
|||||||
<div key={item.index} className={`corpse-item ${!item.can_loot ? 'locked' : ''}`}>
|
<div key={item.index} className={`corpse-item ${!item.can_loot ? 'locked' : ''}`}>
|
||||||
<div className="corpse-item-info">
|
<div className="corpse-item-info">
|
||||||
<div className="corpse-item-name">
|
<div className="corpse-item-name">
|
||||||
{item.emoji} {item.item_name}
|
{item.emoji} {getTranslatedText(item.item_name)}
|
||||||
</div>
|
</div>
|
||||||
<div className="corpse-item-qty">
|
<div className="corpse-item-qty">
|
||||||
Qty: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
|
Qty: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
|
||||||
</div>
|
</div>
|
||||||
{item.required_tool && (
|
{item.required_tool && (
|
||||||
<div className={`corpse-item-tool ${item.has_tool ? 'has-tool' : 'needs-tool'}`}>
|
<div className={`corpse-item-tool ${item.has_tool ? 'has-tool' : 'needs-tool'}`}>
|
||||||
🔧 {item.required_tool_name} {item.has_tool ? '✓' : '✗'}
|
🔧 {getTranslatedText(item.required_tool_name)} {item.has_tool ? '✓' : '✗'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -256,7 +256,7 @@ function LocationView({
|
|||||||
className="corpse-item-loot-btn"
|
className="corpse-item-loot-btn"
|
||||||
onClick={() => onLootCorpseItem(String(corpse.id), item.index)}
|
onClick={() => onLootCorpseItem(String(corpse.id), item.index)}
|
||||||
disabled={!item.can_loot}
|
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' : '🔒'}
|
{item.can_loot ? '📦 Loot' : '🔒'}
|
||||||
</button>
|
</button>
|
||||||
@@ -286,7 +286,7 @@ function LocationView({
|
|||||||
<div key={i} className="entity-card npc-card">
|
<div key={i} className="entity-card npc-card">
|
||||||
<span className="entity-icon">🧑</span>
|
<span className="entity-icon">🧑</span>
|
||||||
<div className="entity-info">
|
<div className="entity-info">
|
||||||
<div className="entity-name">{npc.name}</div>
|
<div className="entity-name">{getTranslatedText(npc.name)}</div>
|
||||||
{npc.level && <div className="entity-level">Lv. {npc.level}</div>}
|
{npc.level && <div className="entity-level">Lv. {npc.level}</div>}
|
||||||
</div>
|
</div>
|
||||||
<button className="entity-action-btn">Talk</button>
|
<button className="entity-action-btn">Talk</button>
|
||||||
@@ -306,7 +306,7 @@ function LocationView({
|
|||||||
{item.image_path ? (
|
{item.image_path ? (
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(item.image_path)}
|
src={getAssetPath(item.image_path)}
|
||||||
alt={item.name}
|
alt={getTranslatedText(item.name)}
|
||||||
className="entity-icon"
|
className="entity-icon"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
@@ -318,14 +318,14 @@ function LocationView({
|
|||||||
<span className={`entity-icon ${item.image_path ? 'hidden' : ''}`}>{item.emoji || '📦'}</span>
|
<span className={`entity-icon ${item.image_path ? 'hidden' : ''}`}>{item.emoji || '📦'}</span>
|
||||||
<div className="entity-info">
|
<div className="entity-info">
|
||||||
<div className={`entity-name ${item.tier ? `tier-${item.tier}` : ''}`}>
|
<div className={`entity-name ${item.tier ? `tier-${item.tier}` : ''}`}>
|
||||||
{item.name || 'Unknown Item'}
|
{getTranslatedText(item.name) || 'Unknown Item'}
|
||||||
</div>
|
</div>
|
||||||
{item.quantity > 1 && <div className="entity-quantity">×{item.quantity}</div>}
|
{item.quantity > 1 && <div className="entity-quantity">×{item.quantity}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="item-info-btn-container">
|
<div className="item-info-btn-container">
|
||||||
<button className="entity-action-btn info" title="Item Info">Info</button>
|
<button className="entity-action-btn info" title="Item Info">Info</button>
|
||||||
<div className="item-info-tooltip">
|
<div className="item-info-tooltip">
|
||||||
{item.description && <div className="item-tooltip-desc">{item.description}</div>}
|
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||||
{item.weight !== undefined && item.weight > 0 && (
|
{item.weight !== undefined && item.weight > 0 && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
⚖️ Weight: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
|
⚖️ Weight: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
* Asset Path Utility
|
* Asset Path Utility
|
||||||
*
|
*
|
||||||
* Resolves asset paths based on runtime environment:
|
* 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
|
* - Browser: Returns full server URL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Detect if running in Electron
|
// Detect if running in Electron - check at runtime, not module load time
|
||||||
const isElectron = !!(window as any).electronAPI?.isElectron
|
function checkIsElectron(): boolean {
|
||||||
|
return !!(window as any).electronAPI?.isElectron ||
|
||||||
|
window.location.protocol === 'file:'
|
||||||
|
}
|
||||||
|
|
||||||
// Base URL for remote assets (browser mode)
|
// Base URL for remote assets (browser mode)
|
||||||
const ASSET_BASE_URL = import.meta.env.VITE_ASSET_URL ||
|
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 {
|
export function getAssetPath(path: string): string {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
// Normalize path (ensure leading slash)
|
// Normalize path (remove leading slash for Electron compatibility)
|
||||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
const cleanPath = path.startsWith('/') ? path.slice(1) : path
|
||||||
|
|
||||||
if (isElectron) {
|
if (checkIsElectron()) {
|
||||||
// In Electron, assets are served relative to the app
|
// In Electron with base: './', use relative paths
|
||||||
return normalizedPath
|
return `./${cleanPath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// In browser, prepend the server URL
|
// In browser, prepend the server URL
|
||||||
return `${ASSET_BASE_URL}${normalizedPath}`
|
return `${ASSET_BASE_URL}/${cleanPath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we're running in Electron
|
* Check if we're running in Electron
|
||||||
*/
|
*/
|
||||||
export function isElectronApp(): boolean {
|
export function isElectronApp(): boolean {
|
||||||
return isElectron
|
return checkIsElectron()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user