Pre-combat-rewrite: Backup current state before comprehensive combat frontend rewrite
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1a1a1a" />
|
||||
<meta name="description" content="A post-apocalyptic survival RPG" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<title>Echoes of the Ash</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1a1a1a" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
<meta name="description" content="A post-apocalyptic survival RPG" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<title>Echoes of the Ash</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
// Game.tsx - Main game orchestrator (refactored from 3,315 lines to ~350 lines)
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../services/api'
|
||||
import { useGameEngine } from './game/hooks/useGameEngine'
|
||||
import Combat from './game/Combat'
|
||||
@@ -9,6 +10,7 @@ import PlayerSidebar from './game/PlayerSidebar'
|
||||
import './Game.css'
|
||||
|
||||
function Game() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [token] = useState(() => localStorage.getItem('token'))
|
||||
|
||||
// Handle WebSocket messages
|
||||
@@ -23,11 +25,29 @@ function Game() {
|
||||
case 'location_update':
|
||||
// General location updates - update state directly from message data when possible
|
||||
console.log('🗺️ Location update:', message.data?.action, message.data?.message)
|
||||
if (message.data?.message) {
|
||||
actions.addLocationMessage(message.data.message)
|
||||
|
||||
let displayMessage = message.data?.message
|
||||
const action = message.data?.action
|
||||
|
||||
// Handle translations for specific actions
|
||||
if (action === 'enemy_spawned' && message.data.npc_data) {
|
||||
const npcData = message.data.npc_data
|
||||
let npcName = npcData.name
|
||||
if (typeof npcName === 'object' && npcName !== null) {
|
||||
npcName = npcName[i18n.language] || npcName['en'] || npcName['es']
|
||||
}
|
||||
displayMessage = t('messages.enemyAppeared', { name: npcName })
|
||||
} else if (action === 'enemy_despawned') {
|
||||
displayMessage = t('messages.enemyDespawned')
|
||||
} else if (action === 'corpses_decayed' && message.data.count) {
|
||||
displayMessage = t('messages.corpsesDecayed', { count: message.data.count })
|
||||
} else if (action === 'items_decayed' && message.data.count) {
|
||||
displayMessage = t('messages.itemsDecayed', { count: message.data.count })
|
||||
}
|
||||
|
||||
const action = message.data?.action
|
||||
if (displayMessage) {
|
||||
actions.addLocationMessage(displayMessage)
|
||||
}
|
||||
if (action === 'player_arrived' && message.data.player_id) {
|
||||
// Add player to location directly without API call
|
||||
actions.addPlayerToLocation({
|
||||
@@ -326,6 +346,7 @@ function Game() {
|
||||
{/* Location view (when not in combat) */}
|
||||
{!state.combatState && state.location && state.playerState && (
|
||||
<LocationView
|
||||
key={state.location.id}
|
||||
location={state.location}
|
||||
playerState={state.playerState}
|
||||
combatState={state.combatState || null}
|
||||
|
||||
@@ -11,6 +11,9 @@ function LanguageSelector() {
|
||||
|
||||
const changeLanguage = (langCode: string) => {
|
||||
i18n.changeLanguage(langCode)
|
||||
// Reload page to ensure all components refresh with new language
|
||||
// This is necessary because some data comes from API and won't update without refetch
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const currentLang = languages.find(l => l.code === i18n.language) || languages[0]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import CombatView from './CombatView'
|
||||
import { CombatState, CombatLogEntry, Profile, Equipment, PlayerState } from './types'
|
||||
import api from '../../services/api'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
import './CombatEffects.css'
|
||||
|
||||
interface CombatProps {
|
||||
@@ -46,6 +47,35 @@ const Combat = ({
|
||||
|
||||
// Turn timer state for PvE combat
|
||||
const [turnTimeRemaining, setTurnTimeRemaining] = useState<number | null>(null)
|
||||
const isMounted = useRef(true)
|
||||
|
||||
// Floating text ID counter to ensure unique IDs
|
||||
const floatingTextIdCounter = useRef(0)
|
||||
|
||||
// Track all timeout IDs for cleanup
|
||||
const floatingTextTimeouts = useRef<Set<NodeJS.Timeout>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
// Cancel all pending floating text timeouts to prevent DOM manipulation errors
|
||||
floatingTextTimeouts.current.forEach(timeout => clearTimeout(timeout))
|
||||
floatingTextTimeouts.current.clear()
|
||||
// Clear all floating texts on unmount to prevent DOM manipulation errors
|
||||
setFloatingTexts([])
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Clean up floating texts when combat ends
|
||||
useEffect(() => {
|
||||
if (combatState.combat_over) {
|
||||
// Cancel all pending timeouts immediately when combat ends
|
||||
floatingTextTimeouts.current.forEach(timeout => clearTimeout(timeout))
|
||||
floatingTextTimeouts.current.clear()
|
||||
// Clear all floating texts
|
||||
setFloatingTexts([])
|
||||
}
|
||||
}, [combatState.combat_over])
|
||||
|
||||
// PvP Timer Effect
|
||||
useEffect(() => {
|
||||
@@ -110,11 +140,17 @@ const Combat = ({
|
||||
}, [turnTimeRemaining, combatState, updateCombatState])
|
||||
|
||||
const addFloatingText = (text: string, x: number, y: number, type: 'damage-player' | 'damage-enemy' | 'damage-player-dealt' | 'heal') => {
|
||||
const id = Date.now() + Math.random()
|
||||
const id = ++floatingTextIdCounter.current
|
||||
setFloatingTexts(prev => [...prev, { id, text, x, y, type }])
|
||||
setTimeout(() => {
|
||||
setFloatingTexts(prev => prev.filter(ft => ft.id !== id))
|
||||
const timeout = setTimeout(() => {
|
||||
if (isMounted.current) {
|
||||
setFloatingTexts(prev => prev.filter(ft => ft.id !== id))
|
||||
// Remove this timeout from the tracking set
|
||||
floatingTextTimeouts.current.delete(timeout)
|
||||
}
|
||||
}, 2500)
|
||||
// Track this timeout for cleanup
|
||||
floatingTextTimeouts.current.add(timeout)
|
||||
}
|
||||
|
||||
const handlePvEAction = async (action: string) => {
|
||||
@@ -130,38 +166,73 @@ const Combat = ({
|
||||
const messages = data.message.split('\n').filter((m: string) => m.trim())
|
||||
|
||||
// Handle failed flee special case - split combined message
|
||||
const processedMessages: string[] = []
|
||||
const processedMessages: any[] = []
|
||||
messages.forEach((msg: string) => {
|
||||
// Try to parse as JSON first (for structured messages)
|
||||
try {
|
||||
// Check if it looks like a JSON object before trying to parse
|
||||
if (msg.trim().startsWith('{')) {
|
||||
const parsed = JSON.parse(msg)
|
||||
if (parsed.type && parsed.data) {
|
||||
processedMessages.push(parsed) // Push object directly
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Not valid JSON, treat as string
|
||||
}
|
||||
|
||||
// Check if message contains both flee failure and enemy attack
|
||||
const fleeFailMatch = msg.match(/^(Failed to flee!)\s+(.+)$/)
|
||||
if (fleeFailMatch) {
|
||||
processedMessages.push(fleeFailMatch[1]) // "Failed to flee!"
|
||||
processedMessages.push(fleeFailMatch[2]) // Enemy attack message
|
||||
|
||||
// The second part might be a JSON string too
|
||||
const secondPart = fleeFailMatch[2]
|
||||
try {
|
||||
if (secondPart.trim().startsWith('{')) {
|
||||
const parsed = JSON.parse(secondPart)
|
||||
if (parsed.type && parsed.data) {
|
||||
processedMessages.push(parsed)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
processedMessages.push(secondPart) // Enemy attack message (string fallback)
|
||||
} else {
|
||||
processedMessages.push(msg)
|
||||
}
|
||||
})
|
||||
|
||||
const playerMessages = processedMessages.filter((msg: string) =>
|
||||
msg.includes('You ') || msg.includes('Your ') || msg === 'Failed to flee!'
|
||||
)
|
||||
const enemyMessages = processedMessages.filter((msg: string) =>
|
||||
msg !== 'Failed to flee!' &&
|
||||
(msg.includes('attacks') || msg.includes('hits') || msg.includes('misses') || msg.includes('The '))
|
||||
)
|
||||
const playerMessages = processedMessages.filter((msg: any) => {
|
||||
if (typeof msg === 'object') {
|
||||
return msg.type === 'player_attack' || msg.type === 'victory' || msg.type === 'combat_start'
|
||||
}
|
||||
return msg.includes('You ') || msg.includes('Your ') || msg === 'Failed to flee!'
|
||||
})
|
||||
|
||||
const enemyMessages = processedMessages.filter((msg: any) => {
|
||||
if (typeof msg === 'object') {
|
||||
return msg.type === 'enemy_attack' || msg.type === 'flee_fail'
|
||||
}
|
||||
return msg !== 'Failed to flee!' &&
|
||||
(msg.includes('attacks') || msg.includes('hits') || msg.includes('misses') || msg.includes('The '))
|
||||
})
|
||||
|
||||
// Check if this is a failed flee attempt
|
||||
const isFailedFlee = playerMessages.some(msg => msg === 'Failed to flee!')
|
||||
|
||||
// 1. Immediate Player Feedback
|
||||
playerMessages.forEach((msg: string) => {
|
||||
addCombatLogEntry({ time: timeStr, message: msg, isPlayer: true })
|
||||
playerMessages.forEach((msg: any) => {
|
||||
const logId = `player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
addCombatLogEntry({ id: logId, time: timeStr, message: msg, isPlayer: true })
|
||||
|
||||
// Only show attack animations for actual attacks, not flee failures
|
||||
if (msg !== 'Failed to flee!') {
|
||||
const damageMatch = msg.match(/(\d+) damage/)
|
||||
if (damageMatch) {
|
||||
addFloatingText(damageMatch[1], 50, 30, 'damage-player-dealt') // White text on enemy
|
||||
if (msg !== 'Failed to flee!' && (typeof msg !== 'object' || msg.type === 'player_attack')) {
|
||||
const damage = typeof msg === 'object' ? msg.data.damage : (msg.match(/(\d+) damage/) || [])[1]
|
||||
if (damage) {
|
||||
addFloatingText(damage.toString(), 50, 30, 'damage-player-dealt') // White text on enemy
|
||||
setFlash(true)
|
||||
setTimeout(() => setFlash(false), 300)
|
||||
}
|
||||
@@ -193,12 +264,13 @@ const Combat = ({
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
enemyMessages.forEach((msg: string) => {
|
||||
addCombatLogEntry({ time: timeStr, message: msg, isPlayer: false })
|
||||
enemyMessages.forEach((msg: any) => {
|
||||
const logId = `enemy-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
addCombatLogEntry({ id: logId, time: timeStr, message: msg, isPlayer: false })
|
||||
|
||||
const damageMatch = msg.match(/(\d+) damage/)
|
||||
if (damageMatch) {
|
||||
addFloatingText(damageMatch[1], 50, 50, 'damage-player') // Red text over player position
|
||||
const damage = typeof msg === 'object' ? msg.data.damage : (msg.match(/(\d+) damage/) || [])[1]
|
||||
if (damage) {
|
||||
addFloatingText(damage.toString(), 50, 50, 'damage-player') // Red text over player position
|
||||
setShake(true)
|
||||
setTimeout(() => setShake(false), 500)
|
||||
}
|
||||
@@ -293,7 +365,8 @@ const Combat = ({
|
||||
// Parse message for damage
|
||||
// Example: "You attacked X for 10 damage!"
|
||||
const msg = data.message || ''
|
||||
addCombatLogEntry({ time: timeStr, message: msg, isPlayer: true })
|
||||
const logId = `pvp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
addCombatLogEntry({ id: logId, time: timeStr, message: msg, isPlayer: true })
|
||||
|
||||
const damageMatch = msg.match(/(\d+) damage/)
|
||||
if (damageMatch) {
|
||||
@@ -324,7 +397,7 @@ const Combat = ({
|
||||
health: tempPlayerHP
|
||||
} : playerState}
|
||||
equipment={equipment}
|
||||
enemyName={combatState.combat?.npc_name || 'Enemy'}
|
||||
enemyName={getTranslatedText(combatState.combat?.npc_name) || 'Enemy'}
|
||||
enemyImage={combatState.combat?.npc_image || combatState.combat_image || ''}
|
||||
enemyTurnMessage={localEnemyTurnMessage}
|
||||
pvpTimeRemaining={pvpTimer}
|
||||
|
||||
393
pwa/src/components/game/CombatView.tsx
Normal file
393
pwa/src/components/game/CombatView.tsx
Normal file
@@ -0,0 +1,393 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { CombatState, CombatLogEntry, Profile, Equipment, PlayerState } from './types'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
|
||||
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) {
|
||||
const { t } = useTranslation()
|
||||
const displayEnemyName = typeof enemyName === 'object' ? getTranslatedText(enemyName) : (enemyName || 'Enemy')
|
||||
|
||||
// Render structured combat messages
|
||||
const renderCombatMessage = (msg: any) => {
|
||||
// Support both old string format and new structured format
|
||||
if (typeof msg === 'string') {
|
||||
return msg // Legacy format
|
||||
}
|
||||
|
||||
if (!msg || !msg.type) {
|
||||
return String(msg)
|
||||
}
|
||||
|
||||
const { type, data } = msg
|
||||
|
||||
switch (type) {
|
||||
case 'combat_start':
|
||||
return t('combat.messages.combat_start', { enemy: getTranslatedText(data.npc_name) })
|
||||
case 'player_attack':
|
||||
return t('combat.messages.player_attack', { damage: data.damage })
|
||||
case 'enemy_attack':
|
||||
return t('combat.messages.enemy_attack', {
|
||||
enemy: getTranslatedText(data.npc_name),
|
||||
damage: data.damage
|
||||
})
|
||||
case 'victory':
|
||||
return t('combat.messages.victory', { enemy: getTranslatedText(data.npc_name) })
|
||||
case 'flee_fail':
|
||||
return t('combat.messages.flee_fail', {
|
||||
enemy: getTranslatedText(data.npc_name),
|
||||
damage: data.damage
|
||||
})
|
||||
default:
|
||||
return JSON.stringify(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="combat-view">
|
||||
<div className="combat-header-inline">
|
||||
<h2>
|
||||
{combatState.is_pvp ? `⚔️ ${t('combat.title')} - PvP` : `⚔️ ${t('combat.title')} - ${displayEnemyName}`}
|
||||
</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">
|
||||
<div className="floating-texts-container">
|
||||
{floatingTexts.map(ft => (
|
||||
<div
|
||||
key={ft.id}
|
||||
className={`floating-text ${ft.type}`}
|
||||
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}>
|
||||
{ft.text}
|
||||
</div>
|
||||
))}
|
||||
</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}
|
||||
>
|
||||
{t('game.attack')}
|
||||
</button>
|
||||
<button
|
||||
className="combat-action-btn flee-btn"
|
||||
onClick={() => onPvPAction('flee')}
|
||||
disabled={!combatState.pvp_combat.your_turn || buttonsDisabled}
|
||||
>
|
||||
{t('game.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">{t('combat.combatLog')}</h3>
|
||||
<div className="combat-log-inline">
|
||||
<div className="log-entries">
|
||||
<div className="log-list">
|
||||
{combatLog.length > 0 ? (
|
||||
combatLog.map((entry: any) => (
|
||||
<div key={entry.id} className={`log-entry ${entry.isPlayer ? 'player-log' : 'enemy-log'}`}>
|
||||
<span className="log-time">[{entry.time}]</span>
|
||||
<span className="log-message">{renderCombatMessage(entry.message)}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="log-entry"><span className="log-message">PvP Combat started...</span></div>
|
||||
)}
|
||||
</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">
|
||||
<div className="floating-texts-container">
|
||||
{floatingTexts.map(ft => (
|
||||
<div
|
||||
key={ft.id}
|
||||
className={`floating-text ${ft.type}`}
|
||||
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}>
|
||||
{ft.text}
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
{t('combat.enemyHp')}: {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' }}>
|
||||
{t('combat.playerHp')}: {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">✅ {t('combat.yourTurn')}</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">⚠️ {t('combat.enemyTurn')}</span>
|
||||
)
|
||||
) : (
|
||||
<span className={combatState.player_won ? "your-turn" : combatState.player_fled ? "your-turn" : "enemy-turn"}>
|
||||
{combatState.player_won ? `✅ ${t('combat.victory')}` : combatState.player_fled ? `🏃 ${t('combat.fleeSuccess')}` : `💀 ${t('combat.defeat')}`}
|
||||
</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}
|
||||
>
|
||||
{t('game.attack')}
|
||||
</button>
|
||||
<button
|
||||
className="combat-action-btn flee-btn"
|
||||
onClick={() => onCombatAction('flee')}
|
||||
disabled={combatState.combat?.turn !== 'player' || !!enemyTurnMessage || buttonsDisabled}
|
||||
>
|
||||
{t('game.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">{t('combat.combatLog')}</h3>
|
||||
<div className="combat-log-inline">
|
||||
<div className="log-entries">
|
||||
<div className="log-list">
|
||||
{combatLog.length > 0 ? (
|
||||
combatLog.map((entry: any) => (
|
||||
<div key={entry.id} className={`log-entry ${entry.isPlayer ? 'player-log' : 'enemy-log'}`}>
|
||||
<span className="log-time">[{entry.time}]</span>
|
||||
<span className="log-message">{renderCombatMessage(entry.message)}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="log-entry"><span className="log-message">Combat started...</span></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CombatView
|
||||
@@ -34,19 +34,19 @@ function InventoryModal({
|
||||
onUnequipItem,
|
||||
onDropItem
|
||||
}: InventoryModalProps) {
|
||||
useTranslation()
|
||||
const { t } = useTranslation()
|
||||
// Categories for the sidebar
|
||||
const categories = [
|
||||
{ id: 'all', label: 'All Items', icon: '🎒' },
|
||||
{ id: 'weapon', label: 'Weapons', icon: '⚔️' },
|
||||
{ id: 'armor', label: 'Armor', icon: '🛡️' },
|
||||
{ id: 'clothing', label: 'Clothing', icon: '👕' },
|
||||
{ id: 'backpack', label: 'Backpacks', icon: '🎒' },
|
||||
{ id: 'tool', label: 'Tools', icon: '🛠️' },
|
||||
{ id: 'consumable', label: 'Consumables', icon: '🍖' },
|
||||
{ id: 'resource', label: 'Resources', icon: '📦' },
|
||||
{ id: 'quest', label: 'Quest', icon: '📜' },
|
||||
{ id: 'misc', label: 'Misc', icon: '📦' }
|
||||
{ id: 'all', label: t('categories.all'), icon: '🎒' },
|
||||
{ id: 'weapon', label: t('categories.weapon'), icon: '⚔️' },
|
||||
{ id: 'armor', label: t('categories.armor'), icon: '🛡️' },
|
||||
{ id: 'clothing', label: t('categories.clothing'), icon: '👕' },
|
||||
{ id: 'backpack', label: t('categories.backpack'), icon: '🎒' },
|
||||
{ id: 'tool', label: t('categories.tool'), icon: '🛠️' },
|
||||
{ id: 'consumable', label: t('categories.consumable'), icon: '🍖' },
|
||||
{ id: 'resource', label: t('categories.resource'), icon: '📦' },
|
||||
{ id: 'quest', label: t('categories.quest'), icon: '📜' },
|
||||
{ id: 'misc', label: t('categories.misc'), icon: '📦' }
|
||||
]
|
||||
|
||||
// Use inventory directly as it now includes equipped items
|
||||
@@ -100,7 +100,7 @@ function InventoryModal({
|
||||
<div className="item-header-compact">
|
||||
<span className="item-emoji-inline">{item.emoji}</span>
|
||||
<h4 className={`item-name-compact text-tier-${item.tier || 0}`}>{getTranslatedText(item.name)}</h4>
|
||||
{item.is_equipped && <span className="item-card-equipped">Equipped</span>}
|
||||
{item.is_equipped && <span className="item-card-equipped">{t('game.equipped')}</span>}
|
||||
</div>
|
||||
|
||||
<div className="item-stats-row">
|
||||
@@ -149,17 +149,17 @@ function InventoryModal({
|
||||
)}
|
||||
{(item.unique_stats?.armor_penetration || item.stats?.armor_penetration) && (
|
||||
<span className="stat-badge penetration">
|
||||
💔 +{item.unique_stats?.armor_penetration || item.stats?.armor_penetration} Pen
|
||||
💔 +{item.unique_stats?.armor_penetration || item.stats?.armor_penetration} {t('stats.pen')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.crit_chance || item.stats?.crit_chance) && (
|
||||
<span className="stat-badge crit">
|
||||
🎯 +{Math.round((item.unique_stats?.crit_chance || item.stats?.crit_chance) * 100)}% Crit
|
||||
🎯 +{Math.round((item.unique_stats?.crit_chance || item.stats?.crit_chance) * 100)}% {t('stats.crit')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.accuracy || item.stats?.accuracy) && (
|
||||
<span className="stat-badge accuracy">
|
||||
👁️ +{Math.round((item.unique_stats?.accuracy || item.stats?.accuracy) * 100)}% Acc
|
||||
👁️ +{Math.round((item.unique_stats?.accuracy || item.stats?.accuracy) * 100)}% {t('stats.acc')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.dodge_chance || item.stats?.dodge_chance) && (
|
||||
@@ -169,34 +169,34 @@ function InventoryModal({
|
||||
)}
|
||||
{(item.unique_stats?.lifesteal || item.stats?.lifesteal) && (
|
||||
<span className="stat-badge lifesteal">
|
||||
🧛 +{Math.round((item.unique_stats?.lifesteal || item.stats?.lifesteal) * 100)}% Life
|
||||
🧛 +{Math.round((item.unique_stats?.lifesteal || item.stats?.lifesteal) * 100)}% {t('stats.life')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Attributes */}
|
||||
{(item.unique_stats?.strength_bonus || item.stats?.strength_bonus) && (
|
||||
<span className="stat-badge strength">
|
||||
💪 +{item.unique_stats?.strength_bonus || item.stats?.strength_bonus} STR
|
||||
💪 +{item.unique_stats?.strength_bonus || item.stats?.strength_bonus} {t('stats.str')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.agility_bonus || item.stats?.agility_bonus) && (
|
||||
<span className="stat-badge agility">
|
||||
🏃 +{item.unique_stats?.agility_bonus || item.stats?.agility_bonus} AGI
|
||||
🏃 +{item.unique_stats?.agility_bonus || item.stats?.agility_bonus} {t('stats.agi')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus) && (
|
||||
<span className="stat-badge endurance">
|
||||
🏋️ +{item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus} END
|
||||
🏋️ +{item.unique_stats?.endurance_bonus || item.stats?.endurance_bonus} {t('stats.end')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.hp_bonus || item.stats?.hp_bonus) && (
|
||||
<span className="stat-badge health">
|
||||
❤️ +{item.unique_stats?.hp_bonus || item.stats?.hp_bonus} HP max
|
||||
❤️ +{item.unique_stats?.hp_bonus || item.stats?.hp_bonus} {t('stats.hpMax')}
|
||||
</span>
|
||||
)}
|
||||
{(item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus) && (
|
||||
<span className="stat-badge stamina">
|
||||
⚡ +{item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus} Stm max
|
||||
⚡ +{item.unique_stats?.stamina_bonus || item.stats?.stamina_bonus} {t('stats.stmMax')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -217,7 +217,7 @@ function InventoryModal({
|
||||
{hasDurability && (
|
||||
<div className="durability-container">
|
||||
<div className="durability-header">
|
||||
<span>Durability</span>
|
||||
<span>{t('game.durability')}</span>
|
||||
<span className={
|
||||
currentDurability < maxDurability * 0.2
|
||||
? "durability-text-low"
|
||||
@@ -246,18 +246,18 @@ function InventoryModal({
|
||||
{/* Right: Actions */}
|
||||
<div className="item-actions-section">
|
||||
{item.consumable && (
|
||||
<button className="action-btn use" onClick={() => onUseItem(item.item_id, item.id)}>Use</button>
|
||||
<button className="action-btn use" onClick={() => onUseItem(item.item_id, item.id)}>{t('game.use')}</button>
|
||||
)}
|
||||
{item.equippable && !item.is_equipped && (
|
||||
<button className="action-btn equip" onClick={() => onEquipItem(item.id)}>Equip</button>
|
||||
<button className="action-btn equip" onClick={() => onEquipItem(item.id)}>{t('game.equip')}</button>
|
||||
)}
|
||||
{item.is_equipped && (
|
||||
<button className="action-btn unequip" onClick={() => onUnequipItem(item.slot)}>Unequip</button>
|
||||
<button className="action-btn unequip" onClick={() => onUnequipItem(item.slot)}>{t('game.unequip')}</button>
|
||||
)}
|
||||
|
||||
<div className="drop-actions-group">
|
||||
{item.quantity > 1 && (
|
||||
<button className="action-btn drop" onClick={() => onDropItem(item.item_id, item.id, 1)}>x1</button>
|
||||
<button className="action-btn drop" onClick={() => onDropItem(item.item_id, item.id, item.quantity)}>{t('game.dropAll')}</button>
|
||||
)}
|
||||
{item.quantity >= 5 && (
|
||||
<button className="action-btn drop" onClick={() => onDropItem(item.item_id, item.id, 5)}>x5</button>
|
||||
@@ -266,7 +266,7 @@ function InventoryModal({
|
||||
<button className="action-btn drop" onClick={() => onDropItem(item.item_id, item.id, 10)}>x10</button>
|
||||
)}
|
||||
<button className={`action-btn drop ${item.quantity === 1 ? 'single' : ''}`} onClick={() => onDropItem(item.item_id, item.id, item.quantity === 1 ? 1 : item.quantity)}>
|
||||
{item.quantity === 1 ? 'Drop' : 'All'}
|
||||
{item.quantity === 1 ? t('game.drop') : t('game.dropAll')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,7 +288,7 @@ function InventoryModal({
|
||||
<span className="metric-icon">⚖️</span>
|
||||
<div className="metric-bar-container">
|
||||
<div className="metric-text">
|
||||
Weight: {(profile.current_weight || 0).toFixed(1)} / {profile.max_weight || 0} kg
|
||||
{t('game.weight')}: {(profile.current_weight || 0).toFixed(1)} / {profile.max_weight || 0} kg
|
||||
</div>
|
||||
<div className="metric-bar">
|
||||
<div
|
||||
@@ -303,7 +303,7 @@ function InventoryModal({
|
||||
<span className="metric-icon">📦</span>
|
||||
<div className="metric-bar-container">
|
||||
<div className="metric-text">
|
||||
Volume: {(profile.current_volume || 0).toFixed(1)} / {profile.max_volume || 0} L
|
||||
{t('game.volume')}: {(profile.current_volume || 0).toFixed(1)} / {profile.max_volume || 0} L
|
||||
</div>
|
||||
<div className="metric-bar">
|
||||
<div
|
||||
@@ -328,7 +328,7 @@ function InventoryModal({
|
||||
) : (
|
||||
<div className="backpack-status inactive">
|
||||
<span className="backpack-icon">🚫</span>
|
||||
<span>No Backpack Equipped</span>
|
||||
<span>{t('game.noBackpack')}</span>
|
||||
</div>
|
||||
)}
|
||||
<button className="close-btn" onClick={onClose}>✕</button>
|
||||
@@ -356,7 +356,7 @@ function InventoryModal({
|
||||
<span className="search-icon">🔍</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search items..."
|
||||
placeholder={t('game.searchItems')}
|
||||
value={inventoryFilter}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => onSetInventoryFilter(e.target.value)}
|
||||
/>
|
||||
@@ -366,32 +366,29 @@ function InventoryModal({
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<span className="empty-icon">📦</span>
|
||||
<p>No items found in this category</p>
|
||||
<p>{t('game.noItemsFound')}</p>
|
||||
</div>
|
||||
) : (
|
||||
inventoryCategoryFilter === 'all' ? (
|
||||
<>
|
||||
{/* Equipped */}
|
||||
{filteredItems.some((i: any) => i.is_equipped) && (
|
||||
{filteredItems.some((item: any) => item.is_equipped) && (
|
||||
<>
|
||||
<div className="category-header">⚔️ Equipped</div>
|
||||
{filteredItems.filter((i: any) => i.is_equipped).map((item: any, i: number) => renderItemCard(item, i))}
|
||||
<div className="category-header">⚔️ {t('game.equipped')}</div>
|
||||
{filteredItems.filter((item: any) => item.is_equipped).map((item: any, i: number) => renderItemCard(item, i))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Categories */}
|
||||
{categories.filter(c => c.id !== 'all').map(cat => {
|
||||
const categoryItems = filteredItems.filter((i: any) => !i.is_equipped && i.type === cat.id);
|
||||
if (categoryItems.length === 0) return null;
|
||||
return (
|
||||
<div key={cat.id}>
|
||||
<div className="category-header">{cat.icon} {cat.label}</div>
|
||||
{categoryItems.map((item: any, i: number) => renderItemCard(item, i))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Backpack */}
|
||||
{filteredItems.some((item: any) => !item.is_equipped) && (
|
||||
<>
|
||||
<div className="category-header">🎒 {t('game.backpack')}</div>
|
||||
{filteredItems.filter((item: any) => !item.is_equipped).map((item: any, i: number) => renderItemCard(item, i))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* Single category */
|
||||
filteredItems.map((item: any, i: number) => renderItemCard(item, i))
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -82,7 +82,7 @@ function LocationView({
|
||||
onRepair,
|
||||
onUncraft
|
||||
}: LocationViewProps) {
|
||||
useTranslation()
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="location-view">
|
||||
<div className="location-info">
|
||||
@@ -115,15 +115,15 @@ function LocationView({
|
||||
onClick={isClickable ? handleClick : undefined}
|
||||
style={isClickable ? { cursor: 'pointer' } : undefined}
|
||||
>
|
||||
{tag === 'workbench' && '🔧 Workbench'}
|
||||
{tag === 'repair_station' && '🛠️ Repair Station'}
|
||||
{tag === 'safe_zone' && '🛡️ Safe Zone'}
|
||||
{tag === 'shop' && '🏪 Shop'}
|
||||
{tag === 'shelter' && '🏠 Shelter'}
|
||||
{tag === 'medical' && '⚕️ Medical'}
|
||||
{tag === 'storage' && '📦 Storage'}
|
||||
{tag === 'water_source' && '💧 Water'}
|
||||
{tag === 'food_source' && '🍎 Food'}
|
||||
{tag === 'workbench' && t('tags.workbench')}
|
||||
{tag === 'repair_station' && t('tags.repairStation')}
|
||||
{tag === 'safe_zone' && t('tags.safeZone')}
|
||||
{tag === 'shop' && t('tags.shop')}
|
||||
{tag === 'shelter' && t('tags.shelter')}
|
||||
{tag === 'medical' && t('tags.medical')}
|
||||
{tag === 'storage' && t('tags.storage')}
|
||||
{tag === 'water_source' && t('tags.water')}
|
||||
{tag === 'food_source' && t('tags.food')}
|
||||
{tag !== 'workbench' && tag !== 'repair_station' && tag !== 'safe_zone' && tag !== 'shop' && tag !== 'shelter' && tag !== 'medical' && tag !== 'storage' && tag !== 'water_source' && tag !== 'food_source' && `🏷️ ${tag}`}
|
||||
</span>
|
||||
)
|
||||
@@ -157,7 +157,7 @@ function LocationView({
|
||||
|
||||
{locationMessages.length > 0 && (
|
||||
<div className="location-messages-log">
|
||||
<h4>📜 Recent Activity</h4>
|
||||
<h4>{t('location.recentActivity')}</h4>
|
||||
<div className="messages-scroll">
|
||||
{locationMessages.slice(-10).reverse().map((msg, idx) => (
|
||||
<div key={idx} className="location-message-item">
|
||||
@@ -173,7 +173,7 @@ function LocationView({
|
||||
{/* Enemies */}
|
||||
{location.npcs.filter((npc: any) => npc.type === 'enemy').length > 0 && (
|
||||
<div className="entity-section enemies-section">
|
||||
<h3>⚔️ Enemies</h3>
|
||||
<h3>{t('location.enemies')}</h3>
|
||||
<div className="entity-list">
|
||||
{location.npcs.filter((npc: any) => npc.type === 'enemy').map((enemy: any, i: number) => (
|
||||
<div key={i} className="entity-card enemy-card">
|
||||
@@ -188,13 +188,13 @@ function LocationView({
|
||||
)}
|
||||
<div className="entity-info">
|
||||
<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">{t('location.level')} {enemy.level}</div>}
|
||||
</div>
|
||||
<button
|
||||
className="entity-action-btn combat-btn"
|
||||
onClick={() => onInitiateCombat(enemy.id)}
|
||||
>
|
||||
⚔️ Fight
|
||||
{t('common.fight')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -205,28 +205,28 @@ function LocationView({
|
||||
{/* Corpses */}
|
||||
{location.corpses && location.corpses.length > 0 && (
|
||||
<div className="entity-section corpses-section">
|
||||
<h3>💀 Corpses</h3>
|
||||
<h3>{t('location.corpses')}</h3>
|
||||
<div className="entity-list">
|
||||
{location.corpses.map((corpse: any) => (
|
||||
<div key={corpse.id} className="corpse-container">
|
||||
<div className="entity-card corpse-card">
|
||||
<div className="entity-info">
|
||||
<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} {t('location.items')}</div>
|
||||
</div>
|
||||
<button
|
||||
className="entity-action-btn loot-btn"
|
||||
onClick={() => onLootCorpse(String(corpse.id))}
|
||||
disabled={corpse.loot_count === 0}
|
||||
>
|
||||
🔍 Examine
|
||||
🔍 {t('common.examine')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expandedCorpse === String(corpse.id) && corpseDetails && corpseDetails.loot_items && (
|
||||
<div className="corpse-details">
|
||||
<div className="corpse-details-header">
|
||||
<h4>Lootable Items:</h4>
|
||||
<h4>{t('location.lootableItems')}</h4>
|
||||
<button
|
||||
className="close-btn"
|
||||
onClick={() => {
|
||||
@@ -244,7 +244,7 @@ function LocationView({
|
||||
{item.emoji} {getTranslatedText(item.item_name)}
|
||||
</div>
|
||||
<div className="corpse-item-qty">
|
||||
Qty: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
|
||||
{t('common.qty')}: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
|
||||
</div>
|
||||
{item.required_tool && (
|
||||
<div className={`corpse-item-tool ${item.has_tool ? 'has-tool' : 'needs-tool'}`}>
|
||||
@@ -258,7 +258,7 @@ function LocationView({
|
||||
disabled={!item.can_loot}
|
||||
title={!item.can_loot ? `Requires ${getTranslatedText(item.required_tool_name)}` : 'Loot this item'}
|
||||
>
|
||||
{item.can_loot ? '📦 Loot' : '🔒'}
|
||||
{item.can_loot ? `📦 ${t('common.loot')}` : '🔒'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -267,7 +267,7 @@ function LocationView({
|
||||
className="loot-all-btn"
|
||||
onClick={() => onLootCorpseItem(String(corpse.id), null)}
|
||||
>
|
||||
📦 Loot All Available
|
||||
📦 {t('common.lootAll')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -280,16 +280,16 @@ function LocationView({
|
||||
{/* Friendly NPCs */}
|
||||
{location.npcs.filter((npc: any) => npc.type !== 'enemy').length > 0 && (
|
||||
<div className="entity-section npcs-section">
|
||||
<h3>👥 NPCs</h3>
|
||||
<h3>{t('location.npcs')}</h3>
|
||||
<div className="entity-list">
|
||||
{location.npcs.filter((npc: any) => npc.type !== 'enemy').map((npc: any, i: number) => (
|
||||
<div key={i} className="entity-card npc-card">
|
||||
<span className="entity-icon">🧑</span>
|
||||
<div className="entity-info">
|
||||
<div className="entity-name">{getTranslatedText(npc.name)}</div>
|
||||
{npc.level && <div className="entity-level">Lv. {npc.level}</div>}
|
||||
{npc.level && <div className="entity-level">{t('location.level')} {npc.level}</div>}
|
||||
</div>
|
||||
<button className="entity-action-btn">Talk</button>
|
||||
<button className="entity-action-btn">{t('common.talk')}</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -299,7 +299,7 @@ function LocationView({
|
||||
{/* Items on Ground */}
|
||||
{location.items.length > 0 && (
|
||||
<div className="entity-section items-section">
|
||||
<h3>📦 Items on Ground</h3>
|
||||
<h3>{t('location.itemsOnGround')}</h3>
|
||||
<div className="entity-list">
|
||||
{location.items.map((item: any, i: number) => (
|
||||
<div key={i} className="entity-card item-card">
|
||||
@@ -323,37 +323,37 @@ function LocationView({
|
||||
{item.quantity > 1 && <div className="entity-quantity">×{item.quantity}</div>}
|
||||
</div>
|
||||
<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">{t('common.info')}</button>
|
||||
<div className="item-info-tooltip">
|
||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||
{item.weight !== undefined && item.weight > 0 && (
|
||||
<div className="item-tooltip-stat">
|
||||
⚖️ Weight: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
|
||||
⚖️ {t('stats.weight')}: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
|
||||
</div>
|
||||
)}
|
||||
{item.volume !== undefined && item.volume > 0 && (
|
||||
<div className="item-tooltip-stat">
|
||||
📦 Volume: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
|
||||
📦 {t('stats.volume')}: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
|
||||
</div>
|
||||
)}
|
||||
{item.hp_restore && item.hp_restore > 0 && (
|
||||
<div className="item-tooltip-stat">❤️ HP Restore: +{item.hp_restore}</div>
|
||||
<div className="item-tooltip-stat">❤️ {t('stats.hpRestore')}: +{item.hp_restore}</div>
|
||||
)}
|
||||
{item.stamina_restore && item.stamina_restore > 0 && (
|
||||
<div className="item-tooltip-stat">⚡ Stamina Restore: +{item.stamina_restore}</div>
|
||||
<div className="item-tooltip-stat">⚡ {t('stats.staminaRestore')}: +{item.stamina_restore}</div>
|
||||
)}
|
||||
{item.damage_min !== undefined && item.damage_max !== undefined && (item.damage_min > 0 || item.damage_max > 0) && (
|
||||
<div className="item-tooltip-stat">
|
||||
⚔️ Damage: {item.damage_min}-{item.damage_max}
|
||||
⚔️ {t('stats.damage')}: {item.damage_min}-{item.damage_max}
|
||||
</div>
|
||||
)}
|
||||
{item.durability !== undefined && item.durability !== null && item.max_durability !== undefined && item.max_durability !== null && (
|
||||
<div className="item-tooltip-stat">
|
||||
🔧 Durability: {item.durability}/{item.max_durability}
|
||||
🔧 {t('stats.durability')}: {item.durability}/{item.max_durability}
|
||||
</div>
|
||||
)}
|
||||
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
||||
<div className="item-tooltip-stat">⭐ Tier: {item.tier}</div>
|
||||
<div className="item-tooltip-stat">⭐ {t('stats.tier')}: {item.tier}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,21 +362,21 @@ function LocationView({
|
||||
className="entity-action-btn pickup"
|
||||
onClick={() => onPickup(item.id, 1)}
|
||||
>
|
||||
Pick Up
|
||||
{t('common.pickUp')}
|
||||
</button>
|
||||
) : (
|
||||
<div className="item-pickup-btn-container">
|
||||
<button className="entity-action-btn pickup">Pick Up ▼</button>
|
||||
<button className="entity-action-btn pickup">{t('common.pickUp')} ▼</button>
|
||||
<div className="item-pickup-menu">
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, 1)}>Pick Up 1</button>
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, 1)}>{t('common.pickUp')} 1</button>
|
||||
{item.quantity >= 5 && (
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, 5)}>Pick Up 5</button>
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, 5)}>{t('common.pickUp')} 5</button>
|
||||
)}
|
||||
{item.quantity >= 10 && (
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, 10)}>Pick Up 10</button>
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, 10)}>{t('common.pickUp')} 10</button>
|
||||
)}
|
||||
<button className="item-pickup-option" onClick={() => onPickup(item.id, item.quantity)}>
|
||||
Pick Up All ({item.quantity})
|
||||
{t('common.pickUpAll')} ({item.quantity})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Location, Profile, CombatState } from './types'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getAssetPath } from '../../utils/assetPath'
|
||||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||
|
||||
@@ -22,6 +23,7 @@ function MovementControls({
|
||||
onMove,
|
||||
onInteract
|
||||
}: MovementControlsProps) {
|
||||
const { t } = useTranslation()
|
||||
// Force re-render every second to update cooldown timers
|
||||
const [, forceUpdate] = useState(0)
|
||||
|
||||
@@ -71,23 +73,24 @@ function MovementControls({
|
||||
const disabled = !available || !!combatState || movementCooldown > 0 || insufficientStamina || (profile?.is_dead ?? false)
|
||||
|
||||
// Build detailed tooltip text
|
||||
const tooltipText = profile?.is_dead ? 'You are dead' :
|
||||
movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` :
|
||||
combatState ? 'Cannot travel during combat' :
|
||||
insufficientStamina ? `Not enough stamina (need ${stamina}, have ${profile?.stamina ?? 0})` :
|
||||
available ? `${destination}\nDistance: ${distance}m\nStamina: ${stamina}` :
|
||||
`Cannot go ${direction}`
|
||||
const tooltipText = profile?.is_dead ? t('messages.youAreDead') :
|
||||
movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) :
|
||||
combatState ? t('messages.cannotTravelCombat') :
|
||||
insufficientStamina ? t('messages.notEnoughStamina', { need: stamina, have: profile?.stamina ?? 0 }) :
|
||||
available ? `${destination}\n${t('game.distance')}: ${distance}m\n${t('game.stamina')}: ${stamina}` :
|
||||
t('messages.cannotGo', { direction: t('directions.' + direction) })
|
||||
|
||||
return (
|
||||
<button
|
||||
key={direction}
|
||||
className={`compass-btn ${className} ${disabled ? 'disabled' : ''} ${combatState ? 'in-combat' : ''}`}
|
||||
onClick={() => onMove(direction)}
|
||||
disabled={disabled}
|
||||
className={`compass-btn ${className} ${disabled ? 'disabled' : ''}`}
|
||||
title={tooltipText}
|
||||
>
|
||||
<span className="compass-arrow">{arrow}</span>
|
||||
{available && movementCooldown > 0 ? (
|
||||
<span className="compass-cost">⏳{movementCooldown}s</span>
|
||||
<span className="compass-cost" title={t('messages.waitBeforeMoving', { seconds: movementCooldown })}>⏳{movementCooldown}s</span>
|
||||
) : available && (
|
||||
<span className="compass-cost">⚡{stamina}</span>
|
||||
)}
|
||||
@@ -98,7 +101,7 @@ function MovementControls({
|
||||
return (
|
||||
<>
|
||||
<div className="movement-controls">
|
||||
<h3>🧭 Travel</h3>
|
||||
<h3>{t('game.travel')}</h3>
|
||||
<div className="compass-grid">
|
||||
{/* Top row */}
|
||||
{renderCompassButton('northwest', '↖️', 'nw')}
|
||||
@@ -121,7 +124,7 @@ function MovementControls({
|
||||
{/* Cooldown indicator */}
|
||||
{movementCooldown > 0 && (
|
||||
<div className="cooldown-indicator">
|
||||
⏳ Wait {movementCooldown}s before moving
|
||||
⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -132,9 +135,9 @@ function MovementControls({
|
||||
onClick={() => onMove('up')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : `Go up\nStamina: ${getStaminaCost('up')}`}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.up')}\n${t('game.stamina')}: ${getStaminaCost('up')}`}
|
||||
>
|
||||
⬆️ 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>
|
||||
)}
|
||||
{location.directions.includes('down') && (
|
||||
@@ -142,9 +145,9 @@ function MovementControls({
|
||||
onClick={() => onMove('down')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : `Go down\nStamina: ${getStaminaCost('down')}`}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.down')}\n${t('game.stamina')}: ${getStaminaCost('down')}`}
|
||||
>
|
||||
⬇️ 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>
|
||||
)}
|
||||
{location.directions.includes('enter') && (
|
||||
@@ -152,9 +155,9 @@ function MovementControls({
|
||||
onClick={() => onMove('enter')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : `Enter\nStamina: ${getStaminaCost('enter')}`}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.enter')}\n${t('game.stamina')}: ${getStaminaCost('enter')}`}
|
||||
>
|
||||
🚪 Enter <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('enter')}`}</span>
|
||||
🚪 {t('directions.enter')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('enter')}`}</span>
|
||||
</button>
|
||||
)}
|
||||
{location.directions.includes('inside') && (
|
||||
@@ -162,9 +165,9 @@ function MovementControls({
|
||||
onClick={() => onMove('inside')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : `Go inside\nStamina: ${getStaminaCost('inside')}`}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.inside')}\n${t('game.stamina')}: ${getStaminaCost('inside')}`}
|
||||
>
|
||||
🚪 Inside <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('inside')}`}</span>
|
||||
🚪 {t('directions.inside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('inside')}`}</span>
|
||||
</button>
|
||||
)}
|
||||
{location.directions.includes('exit') && (
|
||||
@@ -172,9 +175,9 @@ function MovementControls({
|
||||
onClick={() => onMove('exit')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : 'Exit'}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : t('directions.exit')}
|
||||
>
|
||||
🚪 Exit
|
||||
🚪 {t('directions.exit')}
|
||||
</button>
|
||||
)}
|
||||
{location.directions.includes('outside') && (
|
||||
@@ -182,9 +185,9 @@ function MovementControls({
|
||||
onClick={() => onMove('outside')}
|
||||
className="special-btn"
|
||||
disabled={!!combatState || movementCooldown > 0}
|
||||
title={movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : `Go outside\nStamina: ${getStaminaCost('outside')}`}
|
||||
title={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : `${t('directions.outside')}\n${t('game.stamina')}: ${getStaminaCost('outside')}`}
|
||||
>
|
||||
🚪 Outside <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('outside')}`}</span>
|
||||
🚪 {t('directions.outside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('outside')}`}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -193,7 +196,7 @@ function MovementControls({
|
||||
{/* Surroundings - outside movement controls */}
|
||||
{location.interactables && location.interactables.length > 0 && (
|
||||
<div className="interactables-section">
|
||||
<h3>🌿 Surroundings</h3>
|
||||
<h3>{t('game.surroundings')}</h3>
|
||||
{location.interactables.map((interactable: any) => (
|
||||
<div key={interactable.instance_id} className="interactable-card">
|
||||
{interactable.image_path && (
|
||||
|
||||
@@ -50,7 +50,7 @@ function Workbench({
|
||||
onRepair,
|
||||
onUncraft
|
||||
}: WorkbenchProps) {
|
||||
useTranslation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<any>(null)
|
||||
|
||||
@@ -116,8 +116,8 @@ function Workbench({
|
||||
return (
|
||||
<div className="workbench-empty-state">
|
||||
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>🔧</div>
|
||||
<h3>Select an item to view details</h3>
|
||||
<p>Choose an item from the list on the left</p>
|
||||
<h3>{t('crafting.selectItem')}</h3>
|
||||
<p>{t('crafting.chooseFromList')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -155,13 +155,13 @@ function Workbench({
|
||||
<div className="item-stats" style={{ display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
{Object.entries(item.base_stats || item.stats).map(([key, value]) => {
|
||||
const icons: Record<string, string> = {
|
||||
weight_capacity: '⚖️ Weight',
|
||||
volume_capacity: '📦 Volume',
|
||||
armor: '🛡️ Armor',
|
||||
hp_max: '❤️ Max HP',
|
||||
stamina_max: '⚡ Max Stamina',
|
||||
damage_min: '⚔️ Damage Min',
|
||||
damage_max: '⚔️ Damage Max'
|
||||
weight_capacity: `⚖️ ${t('game.weight')}`,
|
||||
volume_capacity: `📦 ${t('game.volume')}`,
|
||||
armor: `🛡️ ${t('stats.armor')}`,
|
||||
hp_max: `❤️ ${t('stats.maxHp')}`,
|
||||
stamina_max: `⚡ ${t('stats.maxStamina')}`,
|
||||
damage_min: `⚔️ ${t('stats.damage')} Min`,
|
||||
damage_max: `⚔️ ${t('stats.damage')} Max`
|
||||
}
|
||||
const label = icons[key] || key.replace('_', ' ')
|
||||
const unit = key.includes('weight') ? 'kg' : key.includes('volume') ? 'L' : ''
|
||||
@@ -173,7 +173,7 @@ function Workbench({
|
||||
})}
|
||||
</div>
|
||||
<p style={{ fontSize: '0.8rem', color: '#888', fontStyle: 'italic', marginTop: '0.5rem', textAlign: 'center' }}>
|
||||
* Potential base stats. Actual stats may vary.
|
||||
* {t('crafting.potentialBaseStats')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -183,13 +183,13 @@ function Workbench({
|
||||
<div className="item-stats" style={{ display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem', flexWrap: 'wrap' }}>
|
||||
{Object.entries(item.unique_item_data?.unique_stats ?? item.unique_item_data ?? item.base_stats ?? item.stats ?? {}).filter(([k]) => !['id', 'item_id', 'durability', 'max_durability', 'created_at', 'tier'].includes(k)).map(([key, value]) => {
|
||||
const icons: Record<string, string> = {
|
||||
weight_capacity: '⚖️ Weight',
|
||||
volume_capacity: '📦 Volume',
|
||||
armor: '🛡️ Armor',
|
||||
hp_max: '❤️ Max HP',
|
||||
stamina_max: '⚡ Max Stamina',
|
||||
damage_min: '⚔️ Damage Min',
|
||||
damage_max: '⚔️ Damage Max'
|
||||
weight_capacity: `⚖️ ${t('game.weight')}`,
|
||||
volume_capacity: `📦 ${t('game.volume')}`,
|
||||
armor: `🛡️ ${t('stats.armor')}`,
|
||||
hp_max: `❤️ ${t('stats.maxHp')}`,
|
||||
stamina_max: `⚡ ${t('stats.maxStamina')}`,
|
||||
damage_min: `⚔️ ${t('stats.damage')} Min`,
|
||||
damage_max: `⚔️ ${t('stats.damage')} Max`
|
||||
}
|
||||
const label = icons[key] || key.replace('_', ' ')
|
||||
const unit = key.includes('weight') ? 'kg' : key.includes('volume') ? 'L' : ''
|
||||
@@ -206,30 +206,28 @@ function Workbench({
|
||||
{workbenchTab === 'craft' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>📊 Requirements</h4>
|
||||
<h4>{t('crafting.requirements')}</h4>
|
||||
|
||||
{item.craft_level && item.craft_level > 1 && (
|
||||
<div className={`requirement-item ${item.meets_level ? 'met' : 'missing'}`}>
|
||||
<span>Level {item.craft_level} Required</span>
|
||||
<span>{item.meets_level ? '✅' : `❌ (Lv. ${profile?.level || 1})`}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={`requirement-item ${item.meets_level ? 'met' : 'missing'}`}>
|
||||
<span>{t('crafting.levelRequired', { level: item.craft_level })}</span>
|
||||
<span>{item.meets_level ? '✅' : `❌ (Lv. ${profile?.level || 1})`}</span>
|
||||
</div>
|
||||
|
||||
{item.tools && item.tools.length > 0 && (
|
||||
<>
|
||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
|
||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>{t('crafting.tools')}</h5>
|
||||
{item.tools.map((tool: any, i: number) => (
|
||||
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
||||
<span>{tool.emoji} {getTranslatedText(tool.name)}</span>
|
||||
<span>
|
||||
{tool.has_tool ? `✅ ${tool.tool_durability}/${tool.max_durability} (Cost: ${tool.durability_cost})` : `❌ Missing (Cost: ${tool.durability_cost})`}
|
||||
{tool.has_tool ? `✅ ${tool.tool_durability}/${tool.max_durability} (${t('crafting.cost')}: ${tool.durability_cost})` : `❌ ${t('crafting.missing')} (${t('crafting.cost')}: ${tool.durability_cost})`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Materials</h5>
|
||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>{t('crafting.materials')}</h5>
|
||||
{item.materials && item.materials.length > 0 ? (
|
||||
item.materials.map((mat: any, i: number) => (
|
||||
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
||||
@@ -239,7 +237,7 @@ function Workbench({
|
||||
))
|
||||
) : (
|
||||
<div className="requirement-item met">
|
||||
<span>No materials required</span>
|
||||
<span>{t('crafting.noMaterialsRequired')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -252,12 +250,12 @@ function Workbench({
|
||||
style={{ width: '100%', padding: '1rem', fontSize: '1.2rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
|
||||
>
|
||||
<span>
|
||||
{!item.meets_level ? `Need Level ${item.craft_level}` :
|
||||
!item.can_craft ? 'Missing Requirements' : '🔨 Craft Item'}
|
||||
{!item.meets_level ? t('crafting.levelRequired', { level: item.craft_level }) :
|
||||
!item.can_craft ? t('crafting.missingRequirements') : t('crafting.craftItem')}
|
||||
</span>
|
||||
{item.can_craft && (
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 5} Stamina
|
||||
{t('crafting.staminaCost', { cost: item.stamina_cost || 5 })}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -268,10 +266,10 @@ function Workbench({
|
||||
{workbenchTab === 'repair' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>🔧 Repair Status</h4>
|
||||
<h4>🔧 {workbenchTab === 'repair' ? t('game.repair') : t('game.salvage')}</h4>
|
||||
|
||||
{!item.needs_repair ? (
|
||||
<p className="repair-info full-durability" style={{ textAlign: 'center', marginBottom: '1rem' }}>✅ Item is in perfect condition</p>
|
||||
<p className="repair-info full-durability" style={{ textAlign: 'center', marginBottom: '1rem' }}>{t('crafting.perfectCondition')}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="repair-preview-text">
|
||||
@@ -333,12 +331,12 @@ function Workbench({
|
||||
style={{ width: '100%', padding: '1rem', fontSize: '1.2rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
|
||||
>
|
||||
<span>
|
||||
{!item.needs_repair ? 'Already Full' :
|
||||
!item.can_repair ? 'Missing Requirements' : '🛠️ Repair Item'}
|
||||
{!item.needs_repair ? t('crafting.alreadyFull') :
|
||||
!item.can_repair ? t('crafting.missingRequirements') : t('crafting.repairItem')}
|
||||
</span>
|
||||
{item.needs_repair && item.can_repair && (
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 3} Stamina
|
||||
{t('crafting.staminaCost', { cost: item.stamina_cost || 3 })}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -349,7 +347,7 @@ function Workbench({
|
||||
{workbenchTab === 'uncraft' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>♻️ Salvage Preview</h4>
|
||||
<h4>♻️ {t('game.salvage')}</h4>
|
||||
|
||||
{/* Show durability bar if we have durability data */}
|
||||
{(item.unique_item_data || item.durability_percent !== undefined) && (
|
||||
@@ -382,7 +380,7 @@ function Workbench({
|
||||
<>
|
||||
{durabilityRatio < 1.0 && (
|
||||
<div className="uncraft-warning" style={{ marginBottom: '1rem', color: '#ffc107' }}>
|
||||
⚠️ Yield reduced by {Math.round((1 - durabilityRatio) * 100)}% due to damage
|
||||
{t('crafting.yieldReduced', { percent: Math.round((1 - durabilityRatio) * 100) })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -409,15 +407,15 @@ function Workbench({
|
||||
className="uncraft-btn"
|
||||
disabled={(profile?.stamina || 0) < (item.stamina_cost || 1)}
|
||||
onClick={() => {
|
||||
if (window.confirm(`Are you sure you want to salvage ${getTranslatedText(item.name)}? This cannot be undone.`)) {
|
||||
if (window.confirm(t('crafting.confirmSalvage', { name: getTranslatedText(item.name) }))) {
|
||||
onUncraft(item.unique_item_id, item.inventory_id)
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%', padding: '1rem', fontSize: '1.2rem', backgroundColor: '#d32f2f', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
|
||||
>
|
||||
<span>♻️ Salvage Item</span>
|
||||
<span>♻️ {t('game.salvage')}</span>
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 2} Stamina
|
||||
{t('crafting.staminaCost', { cost: item.stamina_cost || 2 })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -429,14 +427,14 @@ function Workbench({
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: 'All', icon: '🎒' },
|
||||
{ id: 'weapon', label: 'Weapons', icon: '⚔️' },
|
||||
{ id: 'armor', label: 'Armor', icon: '🛡️' },
|
||||
{ id: 'clothing', label: 'Clothing', icon: '👕' },
|
||||
{ id: 'tool', label: 'Tools', icon: '🛠️' },
|
||||
{ id: 'consumable', label: 'Consumables', icon: '🍖' },
|
||||
{ id: 'resource', label: 'Resources', icon: '📦' },
|
||||
{ id: 'misc', label: 'Misc', icon: '📦' }
|
||||
{ id: 'all', label: t('categories.all'), icon: '🎒' },
|
||||
{ id: 'weapon', label: t('categories.weapon'), icon: '⚔️' },
|
||||
{ id: 'armor', label: t('categories.armor'), icon: '🛡️' },
|
||||
{ id: 'clothing', label: t('categories.clothing'), icon: '👕' },
|
||||
{ id: 'tool', label: t('categories.tool'), icon: '🛠️' },
|
||||
{ id: 'consumable', label: t('categories.consumable'), icon: '🍖' },
|
||||
{ id: 'resource', label: t('categories.resource'), icon: '📦' },
|
||||
{ id: 'misc', label: t('categories.misc'), icon: '📦' }
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -445,25 +443,25 @@ function Workbench({
|
||||
}}>
|
||||
<div className="workbench-menu">
|
||||
<div className="workbench-header">
|
||||
<h3>🔧 Workbench</h3>
|
||||
<h3>{t('game.workbench')}</h3>
|
||||
<div className="workbench-tabs">
|
||||
<button
|
||||
className={`tab-btn ${workbenchTab === 'craft' ? 'active' : ''}`}
|
||||
onClick={() => onSwitchTab('craft')}
|
||||
>
|
||||
🔨 Craft
|
||||
{t('game.craft')}
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${workbenchTab === 'repair' ? 'active' : ''}`}
|
||||
onClick={() => onSwitchTab('repair')}
|
||||
>
|
||||
🛠️ Repair
|
||||
{t('game.repair')}
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${workbenchTab === 'uncraft' ? 'active' : ''}`}
|
||||
onClick={() => onSwitchTab('uncraft')}
|
||||
>
|
||||
♻️ Salvage
|
||||
{t('game.salvage')}
|
||||
</button>
|
||||
</div>
|
||||
<button className="close-btn" onClick={onCloseCrafting}>✕</button>
|
||||
@@ -472,7 +470,7 @@ function Workbench({
|
||||
<div className="workbench-content-grid">
|
||||
{/* Column 1: Categories Sidebar */}
|
||||
<div className="workbench-sidebar">
|
||||
<h4 className="sidebar-title">Categories</h4>
|
||||
<h4 className="sidebar-title">{t('location.lootableItems').replace(':', '')}</h4>
|
||||
<div className="category-list">
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
@@ -492,7 +490,7 @@ function Workbench({
|
||||
<div className="workbench-filters">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Filter items..."
|
||||
placeholder={t('game.searchItems')}
|
||||
value={workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (workbenchTab === 'craft') onSetCraftFilter(e.target.value)
|
||||
@@ -519,9 +517,7 @@ function Workbench({
|
||||
return matchesSearch && matchesCategory
|
||||
}).length === 0 ? (
|
||||
<div className="empty-state">
|
||||
{workbenchTab === 'craft' ? 'No craftable items found.' :
|
||||
workbenchTab === 'repair' ? 'No repairable items found.' :
|
||||
'No salvageable items found.'}
|
||||
{t('game.noItemsFound')}
|
||||
</div>
|
||||
) : (
|
||||
items
|
||||
@@ -573,7 +569,7 @@ function Workbench({
|
||||
>
|
||||
{getTranslatedText(item.name)}
|
||||
</span>
|
||||
{item.location === 'equipped' && <span className="item-card-equipped" style={{ marginLeft: '0.5rem' }}>Equipped</span>}
|
||||
{item.location === 'equipped' && <span className="item-card-equipped" style={{ marginLeft: '0.5rem' }}>{t('game.equipped')}</span>}
|
||||
</div>
|
||||
|
||||
<div className="item-meta-row">
|
||||
|
||||
@@ -217,7 +217,7 @@ export function useGameEngine(
|
||||
}, [])
|
||||
|
||||
const addCombatLogEntry = useCallback((entry: CombatLogEntry) => {
|
||||
setCombatLog((prev: CombatLogEntry[]) => [entry, ...prev])
|
||||
setCombatLog((prev: CombatLogEntry[]) => [{ ...entry, id: entry.id || Date.now() + Math.random() }, ...prev])
|
||||
}, [])
|
||||
|
||||
// Fetch functions
|
||||
@@ -337,6 +337,7 @@ export function useGameEngine(
|
||||
pvpRes.data.pvp_combat.defender :
|
||||
pvpRes.data.pvp_combat.attacker
|
||||
setCombatLog([{
|
||||
id: 'pvp-combat-init',
|
||||
time: timeStr,
|
||||
message: `PvP combat with ${opponent.username} (Lv. ${opponent.level})!`,
|
||||
isPlayer: true
|
||||
@@ -351,6 +352,7 @@ export function useGameEngine(
|
||||
const now = new Date()
|
||||
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
setCombatLog([{
|
||||
id: 'combat-in-progress',
|
||||
time: timeStr,
|
||||
message: 'Combat in progress...',
|
||||
isPlayer: true
|
||||
@@ -402,8 +404,9 @@ export function useGameEngine(
|
||||
const now = new Date()
|
||||
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
setCombatLog([{
|
||||
id: Date.now() + Math.random(),
|
||||
time: timeStr,
|
||||
message: `⚠️ ${encounter.combat.npc_name} ambushes you!`,
|
||||
message: { type: 'combat_start', data: { npc_name: encounter.combat.npc_name } },
|
||||
isPlayer: false
|
||||
}])
|
||||
|
||||
@@ -503,10 +506,23 @@ export function useGameEngine(
|
||||
const now = new Date()
|
||||
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
const messages = data.message.split('\n').filter((m: string) => m.trim())
|
||||
const newEntries = messages.map((msg: string) => ({
|
||||
const parsedMessages = messages.map((msg: string) => {
|
||||
try {
|
||||
if (msg.trim().startsWith('{')) {
|
||||
const parsed = JSON.parse(msg)
|
||||
if (parsed.type && parsed.data) return parsed
|
||||
}
|
||||
} catch (e) { }
|
||||
return msg
|
||||
})
|
||||
|
||||
const newEntries = parsedMessages.map((msg: any) => ({
|
||||
id: `item-use-${Date.now()}-${Math.random()}`,
|
||||
time: timeStr,
|
||||
message: msg,
|
||||
isPlayer: !msg.includes('attacks')
|
||||
isPlayer: typeof msg === 'object'
|
||||
? msg.type !== 'enemy_attack' && msg.type !== 'flee_fail'
|
||||
: !msg.includes('attacks') && !msg.includes('hits')
|
||||
}))
|
||||
setCombatLog((prev: any) => [...newEntries, ...prev])
|
||||
|
||||
@@ -688,8 +704,9 @@ export function useGameEngine(
|
||||
const now = new Date()
|
||||
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
setCombatLog([{
|
||||
id: Date.now() + Math.random(),
|
||||
time: timeStr,
|
||||
message: `Combat started with ${response.data.combat.npc_name}!`,
|
||||
message: { type: 'combat_start', data: { npc_name: response.data.combat.npc_name } },
|
||||
isPlayer: true
|
||||
}])
|
||||
|
||||
|
||||
@@ -56,8 +56,9 @@ export interface Profile {
|
||||
}
|
||||
|
||||
export interface CombatLogEntry {
|
||||
id: string | number
|
||||
time: string
|
||||
message: string
|
||||
message: string | { type: string; data: any }
|
||||
isPlayer: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,16 @@
|
||||
"no": "No",
|
||||
"game": "Game",
|
||||
"leaderboards": "Leaderboards",
|
||||
"account": "Account"
|
||||
"account": "Account",
|
||||
"info": "Info",
|
||||
"talk": "Talk",
|
||||
"loot": "Loot",
|
||||
"lootAll": "Loot All Available",
|
||||
"examine": "Examine",
|
||||
"fight": "Fight",
|
||||
"pickUp": "Pick Up",
|
||||
"pickUpAll": "Pick Up All",
|
||||
"qty": "Qty"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
@@ -22,7 +31,23 @@
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"createAccount": "Create Account",
|
||||
"alreadyHaveAccount": "Already have an account?",
|
||||
"dontHaveAccount": "Don't have an account?"
|
||||
"dontHaveAccount": "Don't have an account?",
|
||||
"rememberMe": "Remember me",
|
||||
"loginTitle": "Welcome Back",
|
||||
"registerTitle": "Create Account",
|
||||
"loginSubtitle": "Sign in to continue your journey",
|
||||
"registerSubtitle": "Join the survivors"
|
||||
},
|
||||
"characters": {
|
||||
"title": "Select Character",
|
||||
"createNew": "Create New Character",
|
||||
"play": "Play",
|
||||
"delete": "Delete",
|
||||
"noCharacters": "No characters yet",
|
||||
"createFirst": "Create your first character to begin",
|
||||
"name": "Character Name",
|
||||
"class": "Class",
|
||||
"level": "Level"
|
||||
},
|
||||
"game": {
|
||||
"travel": "🧭 Travel",
|
||||
@@ -40,10 +65,41 @@
|
||||
"use": "Use",
|
||||
"equip": "Equip",
|
||||
"unequip": "Unequip",
|
||||
"attack": "Attack",
|
||||
"flee": "Flee",
|
||||
"attack": "⚔️ Attack",
|
||||
"flee": "🏃 Flee",
|
||||
"rest": "Rest",
|
||||
"onlineCount": "{{count}} Online"
|
||||
"onlineCount": "{{count}} Online",
|
||||
"searchItems": "Search items...",
|
||||
"equipped": "Equipped",
|
||||
"backpack": "Backpack",
|
||||
"noBackpack": "No Backpack Equipped",
|
||||
"distance": "Distance",
|
||||
"stamina": "Stamina",
|
||||
"weight": "Weight",
|
||||
"volume": "Volume",
|
||||
"durability": "Durability",
|
||||
"noItemsFound": "No items found in this category"
|
||||
},
|
||||
"location": {
|
||||
"recentActivity": "📜 Recent Activity",
|
||||
"enemies": "⚔️ Enemies",
|
||||
"corpses": "💀 Corpses",
|
||||
"npcs": "👥 NPCs",
|
||||
"itemsOnGround": "📦 Items on Ground",
|
||||
"lootableItems": "Lootable Items:",
|
||||
"items": "item(s)",
|
||||
"level": "Lv."
|
||||
},
|
||||
"tags": {
|
||||
"workbench": "🔧 Workbench",
|
||||
"repairStation": "🛠️ Repair Station",
|
||||
"safeZone": "🛡️ Safe Zone",
|
||||
"shop": "🏪 Shop",
|
||||
"shelter": "🏠 Shelter",
|
||||
"medical": "⚕️ Medical",
|
||||
"storage": "📦 Storage",
|
||||
"water": "💧 Water",
|
||||
"food": "🍎 Food"
|
||||
},
|
||||
"stats": {
|
||||
"hp": "❤️ HP",
|
||||
@@ -53,8 +109,8 @@
|
||||
"xp": "⭐ XP",
|
||||
"level": "Level",
|
||||
"unspentPoints": "⭐ Unspent",
|
||||
"weight": "⚖️ Weight",
|
||||
"volume": "📦 Volume",
|
||||
"weight": "Weight",
|
||||
"volume": "Volume",
|
||||
"strength": "💪 STR",
|
||||
"strengthFull": "Strength",
|
||||
"strengthDesc": "Increases melee damage and carry capacity",
|
||||
@@ -68,10 +124,23 @@
|
||||
"intellectFull": "Intellect",
|
||||
"intellectDesc": "Enhances crafting and resource gathering",
|
||||
"armor": "🛡️ Armor",
|
||||
"damage": "⚔️ Damage",
|
||||
"durability": "Durability"
|
||||
"damage": "Damage",
|
||||
"durability": "Durability",
|
||||
"tier": "Tier",
|
||||
"hpRestore": "HP Restore",
|
||||
"staminaRestore": "Stamina Restore",
|
||||
"pen": "Pen",
|
||||
"crit": "Crit",
|
||||
"acc": "Acc",
|
||||
"life": "Life",
|
||||
"str": "STR",
|
||||
"agi": "AGI",
|
||||
"end": "END",
|
||||
"hpMax": "HP max",
|
||||
"stmMax": "Stm max"
|
||||
},
|
||||
"combat": {
|
||||
"title": "Combat",
|
||||
"inCombat": "In Combat",
|
||||
"yourTurn": "Your Turn",
|
||||
"enemyTurn": "Enemy's Turn",
|
||||
@@ -80,7 +149,20 @@
|
||||
"youDied": "You Died",
|
||||
"respawn": "Respawn",
|
||||
"fleeSuccess": "You escaped!",
|
||||
"fleeFailed": "Failed to escape!"
|
||||
"fleeFailed": "Failed to escape!",
|
||||
"enemyHp": "Enemy HP",
|
||||
"playerHp": "Your HP",
|
||||
"combatLog": "Combat Log",
|
||||
"attacking": "Attacking",
|
||||
"defending": "Defending",
|
||||
"messages": {
|
||||
"combat_start": "Combat started with {{enemy}}!",
|
||||
"player_attack": "You attack for {{damage}} damage!",
|
||||
"enemy_attack": "{{enemy}} attacks for {{damage}} damage!",
|
||||
"victory": "Victory! Defeated {{enemy}}",
|
||||
"flee_fail": "Failed to flee! {{enemy}} attacks for {{damage}} damage!"
|
||||
},
|
||||
"turnTimer": "Turn Timer"
|
||||
},
|
||||
"equipment": {
|
||||
"head": "Head",
|
||||
@@ -104,7 +186,16 @@
|
||||
"staminaCost": "⚡ {{cost}} Stamina",
|
||||
"alreadyFull": "Already Full",
|
||||
"perfectCondition": "✅ Item is in perfect condition",
|
||||
"yieldReduced": "⚠️ Yield reduced by {{percent}}% due to damage"
|
||||
"yieldReduced": "⚠️ Yield reduced by {{percent}}% due to damage",
|
||||
"selectItem": "Select an item to view details",
|
||||
"chooseFromList": "Choose an item from the list on the left",
|
||||
"yield": "Yield",
|
||||
"repairCost": "Repair Cost",
|
||||
"noMaterialsRequired": "No materials required",
|
||||
"missing": "Missing",
|
||||
"cost": "Cost",
|
||||
"potentialBaseStats": "Potential base stats. Actual stats may vary.",
|
||||
"confirmSalvage": "Are you sure you want to salvage {{name}}? This cannot be undone."
|
||||
},
|
||||
"categories": {
|
||||
"all": "All Items",
|
||||
@@ -119,13 +210,38 @@
|
||||
"misc": "Misc"
|
||||
},
|
||||
"messages": {
|
||||
"notEnoughStamina": "Not enough stamina",
|
||||
"notEnoughStamina": "Not enough stamina (need {{need}}, have {{have}})",
|
||||
"inventoryFull": "Inventory full",
|
||||
"itemDropped": "Item dropped",
|
||||
"itemPickedUp": "Item picked up",
|
||||
"waitBeforeMoving": "Wait {{seconds}}s before moving",
|
||||
"cannotTravelInCombat": "Cannot travel during combat",
|
||||
"cannotInteractInCombat": "Cannot interact during combat"
|
||||
"cannotInteractInCombat": "Cannot interact during combat",
|
||||
"interactionCooldown": "Wait {{seconds}}s before interacting again",
|
||||
"youAreDead": "You are dead",
|
||||
"cannotTravelCombat": "Cannot travel during combat",
|
||||
"cannotGo": "Cannot go {{direction}}",
|
||||
"enemyAppeared": "A {{name}} has appeared!",
|
||||
"enemyDespawned": "A wandering enemy has left the area",
|
||||
"corpsesDecayed": "{{count}} corpses have decayed",
|
||||
"itemsDecayed": "{{count}} dropped items have decayed",
|
||||
"waitBeforeMovingSimple": "Wait {{seconds}}s before moving"
|
||||
},
|
||||
"directions": {
|
||||
"north": "North",
|
||||
"south": "South",
|
||||
"east": "East",
|
||||
"west": "West",
|
||||
"northeast": "Northeast",
|
||||
"northwest": "Northwest",
|
||||
"southeast": "Southeast",
|
||||
"southwest": "Southwest",
|
||||
"up": "Up",
|
||||
"down": "Down",
|
||||
"inside": "Inside",
|
||||
"outside": "Outside",
|
||||
"enter": "Enter",
|
||||
"exit": "Exit"
|
||||
},
|
||||
"landing": {
|
||||
"heroTitle": "Echoes of the Ash",
|
||||
|
||||
@@ -10,19 +10,44 @@
|
||||
"no": "No",
|
||||
"game": "Juego",
|
||||
"leaderboards": "Clasificación",
|
||||
"account": "Cuenta"
|
||||
"account": "Cuenta",
|
||||
"info": "Info",
|
||||
"talk": "Hablar",
|
||||
"loot": "Saquear",
|
||||
"lootAll": "Saquear Todo",
|
||||
"examine": "Examinar",
|
||||
"fight": "Luchar",
|
||||
"pickUp": "Recoger",
|
||||
"pickUpAll": "Recoger Todo",
|
||||
"qty": "Cant"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Iniciar Sesión",
|
||||
"logout": "Cerrar Sesión",
|
||||
"login": "Iniciar sesión",
|
||||
"logout": "Cerrar sesión",
|
||||
"register": "Registrarse",
|
||||
"username": "Usuario",
|
||||
"password": "Contraseña",
|
||||
"email": "Correo",
|
||||
"email": "Correo electrónico",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"createAccount": "Crear Cuenta",
|
||||
"createAccount": "Crear cuenta",
|
||||
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
||||
"dontHaveAccount": "¿No tienes cuenta?"
|
||||
"dontHaveAccount": "¿No tienes una cuenta?",
|
||||
"rememberMe": "Recordarme",
|
||||
"loginTitle": "Bienvenido de nuevo",
|
||||
"registerTitle": "Crear cuenta",
|
||||
"loginSubtitle": "Inicia sesión para continuar tu viaje",
|
||||
"registerSubtitle": "Únete a los supervivientes"
|
||||
},
|
||||
"characters": {
|
||||
"title": "Seleccionar Personaje",
|
||||
"createNew": "Crear Nuevo Personaje",
|
||||
"play": "Jugar",
|
||||
"delete": "Eliminar",
|
||||
"noCharacters": "Aún no hay personajes",
|
||||
"createFirst": "Crea tu primer personaje para comenzar",
|
||||
"name": "Nombre del Personaje",
|
||||
"class": "Clase",
|
||||
"level": "Nivel"
|
||||
},
|
||||
"game": {
|
||||
"travel": "🧭 Viajar",
|
||||
@@ -33,17 +58,48 @@
|
||||
"workbench": "🔧 Banco de Trabajo",
|
||||
"craft": "🔨 Fabricar",
|
||||
"repair": "🛠️ Reparar",
|
||||
"salvage": "♻️ Desmontar",
|
||||
"salvage": "♻️ Desguazar",
|
||||
"pickUp": "Recoger",
|
||||
"drop": "Soltar",
|
||||
"dropAll": "Todo",
|
||||
"use": "Usar",
|
||||
"equip": "Equipar",
|
||||
"unequip": "Desequipar",
|
||||
"attack": "Atacar",
|
||||
"flee": "Huir",
|
||||
"attack": "⚔️ Atacar",
|
||||
"flee": "🏃 Huir",
|
||||
"rest": "Descansar",
|
||||
"onlineCount": "{{count}} En línea"
|
||||
"onlineCount": "{{count}} En línea",
|
||||
"searchItems": "Buscar objetos...",
|
||||
"equipped": "Equipado",
|
||||
"backpack": "Mochila",
|
||||
"noBackpack": "Sin mochila equipada",
|
||||
"distance": "Distancia",
|
||||
"stamina": "Aguante",
|
||||
"weight": "Peso",
|
||||
"volume": "Volumen",
|
||||
"durability": "Durabilidad",
|
||||
"noItemsFound": "No se encontraron objetos en esta categoría"
|
||||
},
|
||||
"location": {
|
||||
"recentActivity": "📜 Actividad Reciente",
|
||||
"enemies": "⚔️ Enemigos",
|
||||
"corpses": "💀 Cadáveres",
|
||||
"npcs": "👥 NPCs",
|
||||
"itemsOnGround": "📦 Objetos en el Suelo",
|
||||
"lootableItems": "Objetos Saqueables:",
|
||||
"items": "objeto(s)",
|
||||
"level": "Nv."
|
||||
},
|
||||
"tags": {
|
||||
"workbench": "🔧 Banco de Trabajo",
|
||||
"repairStation": "🛠️ Estación de Reparación",
|
||||
"safeZone": "🛡️ Zona Segura",
|
||||
"shop": "🏪 Tienda",
|
||||
"shelter": "🏠 Refugio",
|
||||
"medical": "⚕️ Médico",
|
||||
"storage": "📦 Almacén",
|
||||
"water": "💧 Agua",
|
||||
"food": "🍎 Comida"
|
||||
},
|
||||
"stats": {
|
||||
"hp": "❤️ Vida",
|
||||
@@ -53,34 +109,60 @@
|
||||
"xp": "⭐ XP",
|
||||
"level": "Nivel",
|
||||
"unspentPoints": "⭐ Sin gastar",
|
||||
"weight": "⚖️ Peso",
|
||||
"volume": "📦 Volumen",
|
||||
"weight": "Peso",
|
||||
"volume": "Volumen",
|
||||
"strength": "💪 FUE",
|
||||
"strengthFull": "Fuerza",
|
||||
"strengthDesc": "Aumenta el daño cuerpo a cuerpo y capacidad de carga",
|
||||
"strengthDesc": "Aumenta el daño cuerpo a cuerpo y la capacidad de carga",
|
||||
"agility": "🏃 AGI",
|
||||
"agilityFull": "Agilidad",
|
||||
"agilityDesc": "Mejora la esquiva y golpes críticos",
|
||||
"agilityDesc": "Mejora la probabilidad de esquivar y los golpes críticos",
|
||||
"endurance": "🛡️ RES",
|
||||
"enduranceFull": "Resistencia",
|
||||
"enduranceDesc": "Aumenta la vida y energía",
|
||||
"enduranceDesc": "Aumenta la vida y el aguante",
|
||||
"intellect": "🧠 INT",
|
||||
"intellectFull": "Intelecto",
|
||||
"intellectDesc": "Mejora la fabricación y recolección",
|
||||
"intellectDesc": "Mejora la fabricación y recolección de recursos",
|
||||
"armor": "🛡️ Armadura",
|
||||
"damage": "⚔️ Daño",
|
||||
"durability": "Durabilidad"
|
||||
"damage": "Daño",
|
||||
"durability": "Durabilidad",
|
||||
"tier": "Nivel",
|
||||
"hpRestore": "Restaura Vida",
|
||||
"staminaRestore": "Restaura Aguante",
|
||||
"pen": "Pen",
|
||||
"crit": "Crit",
|
||||
"acc": "Prec",
|
||||
"life": "Vida",
|
||||
"str": "FUE",
|
||||
"agi": "AGI",
|
||||
"end": "RES",
|
||||
"hpMax": "Vida máx",
|
||||
"stmMax": "Agua. máx"
|
||||
},
|
||||
"combat": {
|
||||
"title": "Combate",
|
||||
"inCombat": "En Combate",
|
||||
"yourTurn": "Tu Turno",
|
||||
"enemyTurn": "Turno del Enemigo",
|
||||
"victory": "¡Victoria!",
|
||||
"defeat": "Derrota",
|
||||
"youDied": "Has Muerto",
|
||||
"respawn": "Revivir",
|
||||
"fleeSuccess": "¡Escapaste!",
|
||||
"fleeFailed": "¡No pudiste escapar!"
|
||||
"respawn": "Reaparecer",
|
||||
"fleeSuccess": "¡Has escapado!",
|
||||
"fleeFailed": "¡No has podido escapar!",
|
||||
"enemyHp": "Vida del Enemigo",
|
||||
"playerHp": "Tu Vida",
|
||||
"combatLog": "Registro de Combate",
|
||||
"turnTimer": "Temporizador de Turno",
|
||||
"attacking": "Atacando",
|
||||
"defending": "Defendiendo",
|
||||
"messages": {
|
||||
"combat_start": "¡Combate iniciado con {{enemy}}!",
|
||||
"player_attack": "¡Atacas por {{damage}} de daño!",
|
||||
"enemy_attack": "{{enemy}} ataca por {{damage}} de daño!",
|
||||
"victory": "¡Victoria! Derrotaste a {{enemy}}",
|
||||
"flee_fail": "¡Fallaste al huir! {{enemy}} ataca por {{damage}} de daño!"
|
||||
}
|
||||
},
|
||||
"equipment": {
|
||||
"head": "Cabeza",
|
||||
@@ -96,20 +178,29 @@
|
||||
"requirements": "📊 Requisitos",
|
||||
"materials": "Materiales",
|
||||
"tools": "Herramientas",
|
||||
"levelRequired": "Nivel {{level}} Requerido",
|
||||
"levelRequired": "Requiere Nivel {{level}}",
|
||||
"missingRequirements": "Faltan Requisitos",
|
||||
"craftItem": "🔨 Fabricar",
|
||||
"repairItem": "🛠️ Reparar",
|
||||
"salvageItem": "♻️ Desmontar",
|
||||
"staminaCost": "⚡ {{cost}} Energía",
|
||||
"alreadyFull": "Ya está Completo",
|
||||
"perfectCondition": "✅ El objeto está en perfecto estado",
|
||||
"yieldReduced": "⚠️ Rendimiento reducido {{percent}}% por daño"
|
||||
"salvageItem": "♻️ Desguazar",
|
||||
"staminaCost": "⚡ {{cost}} Aguante",
|
||||
"alreadyFull": "Ya está completo",
|
||||
"perfectCondition": "✅ El objeto está en perfectas condiciones",
|
||||
"yieldReduced": "⚠️ Rendimiento reducido un {{percent}}% por daño",
|
||||
"selectItem": "Selecciona un objeto para ver detalles",
|
||||
"chooseFromList": "Elige un objeto de la lista de la izquierda",
|
||||
"yield": "Rendimiento",
|
||||
"repairCost": "Coste de Reparación",
|
||||
"noMaterialsRequired": "No requiere materiales",
|
||||
"missing": "Falta",
|
||||
"cost": "Coste",
|
||||
"potentialBaseStats": "Estadísticas base potenciales. Las estadísticas reales pueden variar.",
|
||||
"confirmSalvage": "¿Estás seguro de que quieres desguazar {{name}}? Esto no se puede deshacer."
|
||||
},
|
||||
"categories": {
|
||||
"all": "Todos",
|
||||
"all": "Todos los Objetos",
|
||||
"weapon": "Armas",
|
||||
"armor": "Armadura",
|
||||
"armor": "Armaduras",
|
||||
"clothing": "Ropa",
|
||||
"backpack": "Mochilas",
|
||||
"tool": "Herramientas",
|
||||
@@ -119,16 +210,41 @@
|
||||
"misc": "Varios"
|
||||
},
|
||||
"messages": {
|
||||
"notEnoughStamina": "No tienes suficiente energía",
|
||||
"notEnoughStamina": "No tienes suficiente aguante (necesitas {{need}}, tienes {{have}})",
|
||||
"inventoryFull": "Inventario lleno",
|
||||
"itemDropped": "Objeto soltado",
|
||||
"itemPickedUp": "Objeto recogido",
|
||||
"waitBeforeMoving": "Espera {{seconds}}s antes de moverte",
|
||||
"cannotTravelInCombat": "No puedes viajar en combate",
|
||||
"cannotInteractInCombat": "No puedes interactuar en combate"
|
||||
"cannotTravelInCombat": "No puedes viajar durante el combate",
|
||||
"cannotInteractInCombat": "No puedes interactuar durante el combate",
|
||||
"interactionCooldown": "Espera {{seconds}}s antes de interactuar de nuevo",
|
||||
"youAreDead": "Estás muerto",
|
||||
"cannotTravelCombat": "No puedes viajar durante el combate",
|
||||
"cannotGo": "No puedes ir al {{direction}}",
|
||||
"enemyAppeared": "¡Un {{name}} ha aparecido!",
|
||||
"enemyDespawned": "Un enemigo errante ha abandonado el área",
|
||||
"corpsesDecayed": "{{count}} cadáveres se han descompuesto",
|
||||
"itemsDecayed": "{{count}} objetos caídos se han descompuesto",
|
||||
"waitBeforeMovingSimple": "Espera {{seconds}}s antes de moverte"
|
||||
},
|
||||
"directions": {
|
||||
"north": "Norte",
|
||||
"south": "Sur",
|
||||
"east": "Este",
|
||||
"west": "Oeste",
|
||||
"northeast": "Noreste",
|
||||
"northwest": "Noroeste",
|
||||
"southeast": "Sureste",
|
||||
"southwest": "Suroeste",
|
||||
"up": "Arriba",
|
||||
"down": "Abajo",
|
||||
"inside": "Adentro",
|
||||
"outside": "Afuera",
|
||||
"enter": "Entrar",
|
||||
"exit": "Salir"
|
||||
},
|
||||
"landing": {
|
||||
"heroTitle": "Ecos de la Ceniza",
|
||||
"heroTitle": "Ecos de las Cenizas",
|
||||
"heroSubtitle": "Un RPG de supervivencia post-apocalíptico",
|
||||
"playNow": "Jugar Ahora",
|
||||
"features": "Características"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-family: 'Saira Condensed', system-ui, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ const api = axios.create({
|
||||
},
|
||||
})
|
||||
|
||||
// Add request interceptor to include language preference
|
||||
api.interceptors.request.use((config) => {
|
||||
const language = localStorage.getItem('i18nextLng') || 'en'
|
||||
config.headers['Accept-Language'] = language
|
||||
return config
|
||||
})
|
||||
|
||||
// Add token to requests if it exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
|
||||
@@ -4,11 +4,12 @@ import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './', // Use relative paths for Electron file:// protocol
|
||||
base: '/', // Changed from ./ to / for better PWA absolute path resolution
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'auto',
|
||||
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
|
||||
manifest: {
|
||||
name: 'Echoes of the Ash',
|
||||
@@ -40,6 +41,9 @@ export default defineConfig({
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user