Release v0.2.10: Update package-lock.json and CI config
@@ -30,48 +30,48 @@ build:web:
|
|||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
# Build Linux AppImage and .deb
|
# # Build Linux AppImage and .deb
|
||||||
build:linux:
|
# build:linux:
|
||||||
stage: build-desktop
|
# stage: build-desktop
|
||||||
image: electronuserland/builder:wine
|
# image: electronuserland/builder:wine
|
||||||
dependencies:
|
# dependencies:
|
||||||
- build:web
|
# - build:web
|
||||||
script:
|
# script:
|
||||||
- cd pwa
|
# - cd pwa
|
||||||
- npm ci
|
# - npm ci
|
||||||
- npm run electron:build:linux
|
# - npm run electron:build:linux
|
||||||
- echo "=== AppImage size ==="
|
# - echo "=== AppImage size ==="
|
||||||
- ls -lh dist-electron/*.AppImage
|
# - ls -lh dist-electron/*.AppImage
|
||||||
- du -h dist-electron/*.AppImage
|
# - du -h dist-electron/*.AppImage
|
||||||
artifacts:
|
# artifacts:
|
||||||
paths:
|
# paths:
|
||||||
- pwa/dist-electron/*.AppImage
|
# - pwa/dist-electron/*.AppImage
|
||||||
expire_in: 1 week
|
# expire_in: 1 week
|
||||||
name: "linux-appimage-$CI_COMMIT_TAG"
|
# name: "linux-appimage-$CI_COMMIT_TAG"
|
||||||
rules:
|
# rules:
|
||||||
- if: '$CI_COMMIT_TAG'
|
# - if: '$CI_COMMIT_TAG'
|
||||||
tags:
|
# tags:
|
||||||
- docker
|
# - docker
|
||||||
|
|
||||||
# Build Linux .deb (separate job to avoid size limits)
|
# # Build Linux .deb (separate job to avoid size limits)
|
||||||
build:linux-deb:
|
# build:linux-deb:
|
||||||
stage: build-desktop
|
# stage: build-desktop
|
||||||
image: electronuserland/builder:wine
|
# image: electronuserland/builder:wine
|
||||||
dependencies:
|
# dependencies:
|
||||||
- build:web
|
# - build:web
|
||||||
script:
|
# script:
|
||||||
- cd pwa
|
# - cd pwa
|
||||||
- npm ci
|
# - npm ci
|
||||||
- npm run electron:build:linux
|
# - npm run electron:build:linux
|
||||||
artifacts:
|
# artifacts:
|
||||||
paths:
|
# paths:
|
||||||
- pwa/dist-electron/*.deb
|
# - pwa/dist-electron/*.deb
|
||||||
expire_in: 1 week
|
# expire_in: 1 week
|
||||||
name: "linux-deb-$CI_COMMIT_TAG"
|
# name: "linux-deb-$CI_COMMIT_TAG"
|
||||||
rules:
|
# rules:
|
||||||
- if: '$CI_COMMIT_TAG'
|
# - if: '$CI_COMMIT_TAG'
|
||||||
tags:
|
# tags:
|
||||||
- docker
|
# - docker
|
||||||
|
|
||||||
# Build Windows executable
|
# Build Windows executable
|
||||||
build:windows:
|
build:windows:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Loads and manages game items from JSON without bot dependencies.
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional, Union
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@@ -12,8 +12,8 @@ from dataclasses import dataclass
|
|||||||
class Item:
|
class Item:
|
||||||
"""Represents a game item"""
|
"""Represents a game item"""
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: Union[str, Dict[str, str]]
|
||||||
description: str
|
description: Union[str, Dict[str, str]]
|
||||||
type: str
|
type: str
|
||||||
image_path: str = ""
|
image_path: str = ""
|
||||||
emoji: str = "📦"
|
emoji: str = "📦"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import logging
|
|||||||
|
|
||||||
from ..core.security import get_current_user, security, verify_internal_key
|
from ..core.security import get_current_user, security, verify_internal_key
|
||||||
from ..services.models import *
|
from ..services.models import *
|
||||||
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity
|
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string
|
||||||
from .. import database as db
|
from .. import database as db
|
||||||
from ..items import ItemsManager
|
from ..items import ItemsManager
|
||||||
from .. import game_logic
|
from .. import game_logic
|
||||||
@@ -226,6 +226,11 @@ async def get_game_state(current_user: dict = Depends(get_current_user)):
|
|||||||
"encumbrance": item_def.encumbrance,
|
"encumbrance": item_def.encumbrance,
|
||||||
"weapon_effects": item_def.weapon_effects if hasattr(item_def, 'weapon_effects') else {}
|
"weapon_effects": item_def.weapon_effects if hasattr(item_def, 'weapon_effects') else {}
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"❌ Item definition not found for equipped item: {inv_item['item_id']} (slot: {slot})")
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ Inventory item not found for equipped slot: {slot} (ID: {item_data['item_id']})")
|
||||||
|
|
||||||
if slot not in equipment:
|
if slot not in equipment:
|
||||||
equipment[slot] = None
|
equipment[slot] = None
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ Helper utilities for game calculations and common operations.
|
|||||||
Contains distance calculations, stamina costs, capacity calculations, etc.
|
Contains distance calculations, stamina costs, capacity calculations, etc.
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
from typing import Tuple, List, Dict, Any
|
from typing import Tuple, List, Dict, Any, Union
|
||||||
from .. import database as db
|
from .. import database as db
|
||||||
from ..items import ItemsManager
|
from ..items import ItemsManager
|
||||||
|
|
||||||
|
|
||||||
|
def get_locale_string(value: Union[str, Dict[str, str]], lang: str = 'en') -> str:
|
||||||
|
"""Helper to safely get string from i18n object or string."""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value.get(lang) or value.get('en') or str(value)
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
|
def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate distance between two points using Euclidean distance.
|
Calculate distance between two points using Euclidean distance.
|
||||||
@@ -182,7 +189,7 @@ async def reduce_armor_durability(player_id: int, damage_taken: int, items_manag
|
|||||||
# which cascades to the inventory row.
|
# which cascades to the inventory row.
|
||||||
|
|
||||||
broken_armor.append({
|
broken_armor.append({
|
||||||
'name': armor['item_def'].name,
|
'name': get_locale_string(armor['item_def'].name),
|
||||||
'emoji': getattr(armor['item_def'], 'emoji', '🛡️')
|
'emoji': getattr(armor['item_def'], 'emoji', '🛡️')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,7 +221,7 @@ async def consume_tool_durability(user_id: int, tools: list, inventory: list, it
|
|||||||
'unique_item_id': inv_item['unique_item_id'],
|
'unique_item_id': inv_item['unique_item_id'],
|
||||||
'item_id': inv_item['item_id'],
|
'item_id': inv_item['item_id'],
|
||||||
'durability': unique_item['durability'],
|
'durability': unique_item['durability'],
|
||||||
'name': item_def.name,
|
'name': get_locale_string(item_def.name),
|
||||||
'emoji': getattr(item_def, 'emoji', '🔧')
|
'emoji': getattr(item_def, 'emoji', '🔧')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ Loads game data from JSON files without bot dependencies.
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional, Union
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Outcome:
|
class Outcome:
|
||||||
"""Represents an outcome of an action"""
|
"""Represents an outcome of an action"""
|
||||||
text: str
|
text: Union[str, Dict[str, str]]
|
||||||
items_reward: Dict[str, int] = field(default_factory=dict)
|
items_reward: Dict[str, int] = field(default_factory=dict)
|
||||||
damage_taken: int = 0
|
damage_taken: int = 0
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class Outcome:
|
|||||||
class Action:
|
class Action:
|
||||||
"""Represents an action that can be performed on an interactable"""
|
"""Represents an action that can be performed on an interactable"""
|
||||||
id: str
|
id: str
|
||||||
label: str
|
label: Union[str, Dict[str, str]]
|
||||||
stamina_cost: int = 2
|
stamina_cost: int = 2
|
||||||
outcomes: Dict[str, Outcome] = field(default_factory=dict)
|
outcomes: Dict[str, Outcome] = field(default_factory=dict)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class Action:
|
|||||||
class Interactable:
|
class Interactable:
|
||||||
"""Represents an interactable object"""
|
"""Represents an interactable object"""
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: Union[str, Dict[str, str]]
|
||||||
image_path: str = ""
|
image_path: str = ""
|
||||||
actions: List[Action] = field(default_factory=list)
|
actions: List[Action] = field(default_factory=list)
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ class Exit:
|
|||||||
class Location:
|
class Location:
|
||||||
"""Represents a location in the game world"""
|
"""Represents a location in the game world"""
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: Union[str, Dict[str, str]]
|
||||||
description: str
|
description: Union[str, Dict[str, str]]
|
||||||
image_path: str = ""
|
image_path: str = ""
|
||||||
exits: Dict[str, str] = field(default_factory=dict) # direction -> destination_id
|
exits: Dict[str, str] = field(default_factory=dict) # direction -> destination_id
|
||||||
exit_stamina: Dict[str, int] = field(default_factory=dict) # direction -> stamina_cost
|
exit_stamina: Dict[str, int] = field(default_factory=dict) # direction -> stamina_cost
|
||||||
|
|||||||
@@ -2,114 +2,192 @@
|
|||||||
"interactables": {
|
"interactables": {
|
||||||
"rubble": {
|
"rubble": {
|
||||||
"id": "rubble",
|
"id": "rubble",
|
||||||
"name": "🧱 Pile of Rubble",
|
"name": {
|
||||||
"description": "A scattered pile of debris and broken concrete.",
|
"en": "🧱 Pile of Rubble",
|
||||||
|
"es": "🧱 Pila de escombros"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A scattered pile of debris and broken concrete.",
|
||||||
|
"es": "Una pila de escombros y cemento roto."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/rubble.webp",
|
"image_path": "images/interactables/rubble.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search": {
|
"search": {
|
||||||
"id": "search",
|
"id": "search",
|
||||||
"label": "\ud83d\udd0e Search Rubble",
|
"label": {
|
||||||
|
"en": "🔎 Search Rubble",
|
||||||
|
"es": "🔎 Buscar en los escombros"
|
||||||
|
},
|
||||||
"stamina_cost": 2
|
"stamina_cost": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dumpster": {
|
"dumpster": {
|
||||||
"id": "dumpster",
|
"id": "dumpster",
|
||||||
"name": "\ud83d\uddd1\ufe0f Dumpster",
|
"name": {
|
||||||
"description": "A rusted metal dumpster, possibly containing scavenged goods.",
|
"en": "🗑️ Dumpster",
|
||||||
|
"es": "🗑️ Contenedor de basura"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A rusted metal dumpster, possibly containing scavenged goods.",
|
||||||
|
"es": "Un contenedor de basura de metal oxidado, posiblemente conteniendo bienes robados."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/dumpster.webp",
|
"image_path": "images/interactables/dumpster.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search_dumpster": {
|
"search_dumpster": {
|
||||||
"id": "search_dumpster",
|
"id": "search_dumpster",
|
||||||
"label": "\ud83d\udd0e Dig Through Trash",
|
"label": {
|
||||||
|
"en": "🔎 Dig Through Trash",
|
||||||
|
"es": "🔎 Buscar en la basura"
|
||||||
|
},
|
||||||
"stamina_cost": 2
|
"stamina_cost": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sedan": {
|
"sedan": {
|
||||||
"id": "sedan",
|
"id": "sedan",
|
||||||
"name": "\ud83d\ude97 Rusty Sedan",
|
"name": {
|
||||||
"description": "An abandoned sedan with rusted doors.",
|
"en": "🚗 Rusty Sedan",
|
||||||
|
"es": "🚗 Sedán oxidado"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "An abandoned sedan with rusted doors.",
|
||||||
|
"es": "Un sedán abandonado con puertas oxidadas."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/sedan.webp",
|
"image_path": "images/interactables/sedan.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search_glovebox": {
|
"search_glovebox": {
|
||||||
"id": "search_glovebox",
|
"id": "search_glovebox",
|
||||||
"label": "\ud83d\udd0e Search Glovebox",
|
"label": {
|
||||||
|
"en": "🔎 Search Glovebox",
|
||||||
|
"es": "🔎 Buscar en la guantera"
|
||||||
|
},
|
||||||
"stamina_cost": 1
|
"stamina_cost": 1
|
||||||
},
|
},
|
||||||
"pop_trunk": {
|
"pop_trunk": {
|
||||||
"id": "pop_trunk",
|
"id": "pop_trunk",
|
||||||
"label": "\ud83d\udd27 Pop the Trunk",
|
"label": {
|
||||||
|
"en": "🔧 Pop the Trunk",
|
||||||
|
"es": "🔧 Forzar el maletero"
|
||||||
|
},
|
||||||
"stamina_cost": 3
|
"stamina_cost": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"house": {
|
"house": {
|
||||||
"id": "house",
|
"id": "house",
|
||||||
"name": "\ud83c\udfda\ufe0f Abandoned House",
|
"name": {
|
||||||
"description": "A dilapidated house with boarded windows.",
|
"en": "🏚️ Abandoned House",
|
||||||
|
"es": "🏚️ Casa abandonada"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A dilapidated house with boarded windows.",
|
||||||
|
"es": "Una casa abandonada con ventanas tapadas."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/house.webp",
|
"image_path": "images/interactables/house.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search_house": {
|
"search_house": {
|
||||||
"id": "search_house",
|
"id": "search_house",
|
||||||
"label": "\ud83d\udd0e Search House",
|
"label": {
|
||||||
|
"en": "🔎 Search House",
|
||||||
|
"es": "🔎 Buscar en la casa"
|
||||||
|
},
|
||||||
"stamina_cost": 3
|
"stamina_cost": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolshed": {
|
"toolshed": {
|
||||||
"id": "toolshed",
|
"id": "toolshed",
|
||||||
"name": "\ud83d\udd28 Tool Shed",
|
"name": {
|
||||||
"description": "A small wooden shed, door slightly ajar.",
|
"en": "🔨 Tool Shed",
|
||||||
|
"es": "🔨 Almacén de herramientas"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A small wooden shed, door slightly ajar.",
|
||||||
|
"es": "Un pequeño almacén de madera, la puerta está ligeramente abierta."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/toolshed.webp",
|
"image_path": "images/interactables/toolshed.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search_shed": {
|
"search_shed": {
|
||||||
"id": "search_shed",
|
"id": "search_shed",
|
||||||
"label": "\ud83d\udd0e Search Shed",
|
"label": {
|
||||||
|
"en": "🔎 Search Shed",
|
||||||
|
"es": "🔎 Buscar en el almacén"
|
||||||
|
},
|
||||||
"stamina_cost": 2
|
"stamina_cost": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"medkit": {
|
"medkit": {
|
||||||
"id": "medkit",
|
"id": "medkit",
|
||||||
"name": "\ud83c\udfe5 Medical Supply Cabinet",
|
"name": {
|
||||||
"description": "A white metal cabinet with a red cross symbol.",
|
"en": "🏥 Medical Supply Cabinet",
|
||||||
|
"es": "🏥 Armario de suministros médicos"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A white metal cabinet with a red cross symbol.",
|
||||||
|
"es": "Un armario de metal blanco con un símbolo de cruz roja."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/medkit.webp",
|
"image_path": "images/interactables/medkit.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search_medkit": {
|
"search_medkit": {
|
||||||
"id": "search_medkit",
|
"id": "search_medkit",
|
||||||
"label": "\ud83d\udd0e Search Cabinet",
|
"label": {
|
||||||
|
"en": "🔎 Search Cabinet",
|
||||||
|
"es": "🔎 Buscar en el armario"
|
||||||
|
},
|
||||||
"stamina_cost": 2
|
"stamina_cost": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storage_box": {
|
"storage_box": {
|
||||||
"id": "storage_box",
|
"id": "storage_box",
|
||||||
"name": "📦 Storage Box",
|
"name": {
|
||||||
"description": "A weathered storage container.",
|
"en": "📦 Storage Box",
|
||||||
|
"es": "📦 Caja de almacenamiento"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A weathered storage container.",
|
||||||
|
"es": "Un contenedor de almacenamiento desgastado."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/storage_box.webp",
|
"image_path": "images/interactables/storage_box.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"search": {
|
"search": {
|
||||||
"id": "search",
|
"id": "search",
|
||||||
"label": "\ud83d\udd0e Search Box",
|
"label": {
|
||||||
|
"en": "🔎 Search Box",
|
||||||
|
"es": "🔎 Buscar en la caja"
|
||||||
|
},
|
||||||
"stamina_cost": 2
|
"stamina_cost": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vending_machine": {
|
"vending_machine": {
|
||||||
"id": "vending_machine",
|
"id": "vending_machine",
|
||||||
"name": "\ud83e\uddc3 Vending Machine",
|
"name": {
|
||||||
"description": "A broken vending machine, glass shattered.",
|
"en": "🧃 Vending Machine",
|
||||||
|
"es": "🧃 Máquina expendedora"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A broken vending machine, glass shattered.",
|
||||||
|
"es": "Una máquina expendedora rota, el vidrio está roto."
|
||||||
|
},
|
||||||
"image_path": "images/interactables/vending.webp",
|
"image_path": "images/interactables/vending.webp",
|
||||||
"actions": {
|
"actions": {
|
||||||
"break": {
|
"break": {
|
||||||
"id": "break",
|
"id": "break",
|
||||||
"label": "\ud83d\udd28 Break Open",
|
"label": {
|
||||||
|
"en": "🔨 Break Open",
|
||||||
|
"es": "🔨 Forzar la máquina"
|
||||||
|
},
|
||||||
"stamina_cost": 5
|
"stamina_cost": 5
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"id": "search",
|
"id": "search",
|
||||||
"label": "\ud83d\udd0e Search Machine",
|
"label": {
|
||||||
|
"en": "🔎 Search Machine",
|
||||||
|
"es": "🔎 Buscar en la máquina"
|
||||||
|
},
|
||||||
"stamina_cost": 2
|
"stamina_cost": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,78 @@
|
|||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"scrap_metal": {
|
"scrap_metal": {
|
||||||
"name": "Scrap Metal",
|
"name": {
|
||||||
|
"en": "Scrap Metal",
|
||||||
|
"es": "Metal desechado"
|
||||||
|
},
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"weight": 0.5,
|
"weight": 0.5,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"emoji": "\u2699\ufe0f",
|
"emoji": "⚙️",
|
||||||
"image_path": "images/items/scrap_metal.webp",
|
"image_path": "images/items/scrap_metal.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rusty_nails": {
|
"rusty_nails": {
|
||||||
"name": "Rusty Nails",
|
"name": {
|
||||||
|
"en": "Rusty Nails",
|
||||||
|
"es": "Clavos oxidados"
|
||||||
|
},
|
||||||
"weight": 0.2,
|
"weight": 0.2,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83d\udccc",
|
"emoji": "📌",
|
||||||
"image_path": "images/items/rusty_nails.webp",
|
"image_path": "images/items/rusty_nails.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"wood_planks": {
|
"wood_planks": {
|
||||||
"name": "Wood Planks",
|
"name": {
|
||||||
|
"en": "Wood Planks",
|
||||||
|
"es": "Tablillas de madera"
|
||||||
|
},
|
||||||
"weight": 3.0,
|
"weight": 3.0,
|
||||||
"volume": 2.0,
|
"volume": 2.0,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83e\udeb5",
|
"emoji": "🪵",
|
||||||
"image_path": "images/items/wood_planks.webp",
|
"image_path": "images/items/wood_planks.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cloth_scraps": {
|
"cloth_scraps": {
|
||||||
"name": "Cloth Scraps",
|
"name": {
|
||||||
|
"en": "Cloth Scraps",
|
||||||
|
"es": "Ramas de tela"
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83e\uddf5",
|
"emoji": "🧵",
|
||||||
"image_path": "images/items/cloth_scraps.webp",
|
"image_path": "images/items/cloth_scraps.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cloth": {
|
"cloth": {
|
||||||
"name": "Cloth",
|
"name": {
|
||||||
|
"en": "Cloth",
|
||||||
|
"es": "Tela"
|
||||||
|
},
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"emoji": "\ud83e\uddf5",
|
"emoji": "🧵",
|
||||||
"description": "A raw material used for crafting and upgrades.",
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
},
|
||||||
"image_path": "images/items/cloth.webp",
|
"image_path": "images/items/cloth.webp",
|
||||||
"uncraftable": true,
|
"uncraftable": true,
|
||||||
"uncraft_yield": [
|
"uncraft_yield": [
|
||||||
@@ -59,187 +89,301 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plastic_bottles": {
|
"plastic_bottles": {
|
||||||
"name": "Plastic Bottles",
|
"name": {
|
||||||
|
"en": "Plastic Bottles",
|
||||||
|
"es": "Botellas de plástico"
|
||||||
|
},
|
||||||
"weight": 0.05,
|
"weight": 0.05,
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83c\udf76",
|
"emoji": "🍶",
|
||||||
"image_path": "images/items/plastic_bottles.webp",
|
"image_path": "images/items/plastic_bottles.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bone": {
|
"bone": {
|
||||||
"name": "Bone",
|
"name": {
|
||||||
|
"en": "Bone",
|
||||||
|
"es": "Hueso"
|
||||||
|
},
|
||||||
"weight": 0.3,
|
"weight": 0.3,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83e\uddb4",
|
"emoji": "🦴",
|
||||||
"image_path": "images/items/bone.webp",
|
"image_path": "images/items/bone.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"raw_meat": {
|
"raw_meat": {
|
||||||
"name": "Raw Meat",
|
"name": {
|
||||||
|
"en": "Raw Meat",
|
||||||
|
"es": "Carne cruda"
|
||||||
|
},
|
||||||
"weight": 0.5,
|
"weight": 0.5,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83e\udd69",
|
"emoji": "🥩",
|
||||||
"image_path": "images/items/raw_meat.webp",
|
"image_path": "images/items/raw_meat.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"animal_hide": {
|
"animal_hide": {
|
||||||
"name": "Animal Hide",
|
"name": {
|
||||||
|
"en": "Animal Hide",
|
||||||
|
"es": "Piel de animal"
|
||||||
|
},
|
||||||
"weight": 0.4,
|
"weight": 0.4,
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83e\udde4",
|
"emoji": "🧤",
|
||||||
"image_path": "images/items/animal_hide.webp",
|
"image_path": "images/items/animal_hide.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"mutant_tissue": {
|
"mutant_tissue": {
|
||||||
"name": "Mutant Tissue",
|
"name": {
|
||||||
|
"en": "Mutant Tissue",
|
||||||
|
"es": "Piel de mutante"
|
||||||
|
},
|
||||||
"weight": 0.2,
|
"weight": 0.2,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\ud83e\uddec",
|
"emoji": "🧬",
|
||||||
"image_path": "images/items/mutant_tissue.webp",
|
"image_path": "images/items/mutant_tissue.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"infected_tissue": {
|
"infected_tissue": {
|
||||||
"name": "Infected Tissue",
|
"name": {
|
||||||
|
"en": "Infected Tissue",
|
||||||
|
"es": "Piel infectada"
|
||||||
|
},
|
||||||
"weight": 0.2,
|
"weight": 0.2,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"emoji": "\u2623\ufe0f",
|
"emoji": "☣️",
|
||||||
"image_path": "images/items/infected_tissue.webp",
|
"image_path": "images/items/infected_tissue.webp",
|
||||||
"description": "A raw material used for crafting and upgrades."
|
"description": {
|
||||||
|
"en": "A raw material used for crafting and upgrades.",
|
||||||
|
"es": "Un material bruto utilizado para la fabricación y las mejoras."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"stale_chocolate_bar": {
|
"stale_chocolate_bar": {
|
||||||
"name": "Stale Chocolate Bar",
|
"name": {
|
||||||
|
"en": "Stale Chocolate Bar",
|
||||||
|
"es": "Barra de chocolate caducada"
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 10,
|
"hp_restore": 10,
|
||||||
"emoji": "\ud83c\udf6b",
|
"emoji": "🍫",
|
||||||
"image_path": "images/items/stale_chocolate_bar.webp",
|
"image_path": "images/items/stale_chocolate_bar.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"canned_beans": {
|
"canned_beans": {
|
||||||
"name": "Canned Beans",
|
"name": {
|
||||||
|
"en": "Canned Beans",
|
||||||
|
"es": "Frijoles enlatados"
|
||||||
|
},
|
||||||
"weight": 0.4,
|
"weight": 0.4,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 20,
|
"hp_restore": 20,
|
||||||
"stamina_restore": 5,
|
"stamina_restore": 5,
|
||||||
"emoji": "\ud83e\udd6b",
|
"emoji": "🥫",
|
||||||
"image_path": "images/items/canned_beans.webp",
|
"image_path": "images/items/canned_beans.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"canned_food": {
|
"canned_food": {
|
||||||
"name": "Canned Food",
|
"name": {
|
||||||
|
"en": "Canned Food",
|
||||||
|
"es": "Comida enlatada"
|
||||||
|
},
|
||||||
"weight": 0.4,
|
"weight": 0.4,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 25,
|
"hp_restore": 25,
|
||||||
"stamina_restore": 5,
|
"stamina_restore": 5,
|
||||||
"emoji": "\ud83e\udd6b",
|
"emoji": "🥫",
|
||||||
"image_path": "images/items/canned_food.webp",
|
"image_path": "images/items/canned_food.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bottled_water": {
|
"bottled_water": {
|
||||||
"name": "Bottled Water",
|
"name": {
|
||||||
|
"en": "Bottled Water",
|
||||||
|
"es": "Agua embotellada"
|
||||||
|
},
|
||||||
"weight": 0.5,
|
"weight": 0.5,
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"stamina_restore": 10,
|
"stamina_restore": 10,
|
||||||
"emoji": "\ud83d\udca7",
|
"emoji": "💧",
|
||||||
"image_path": "images/items/bottled_water.webp",
|
"image_path": "images/items/bottled_water.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"water_bottle": {
|
"water_bottle": {
|
||||||
"name": "Water Bottle",
|
"name": {
|
||||||
|
"en": "Water Bottle",
|
||||||
|
"es": "Botella de agua"
|
||||||
|
},
|
||||||
"weight": 0.5,
|
"weight": 0.5,
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"stamina_restore": 10,
|
"stamina_restore": 10,
|
||||||
"emoji": "\ud83d\udca7",
|
"emoji": "💧",
|
||||||
"image_path": "images/items/water_bottle.webp",
|
"image_path": "images/items/water_bottle.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"energy_bar": {
|
"energy_bar": {
|
||||||
"name": "Energy Bar",
|
"name": {
|
||||||
|
"en": "Energy Bar",
|
||||||
|
"es": "Barra de energía"
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"stamina_restore": 15,
|
"stamina_restore": 15,
|
||||||
"emoji": "\ud83c\udf6b",
|
"emoji": "🍫",
|
||||||
"image_path": "images/items/energy_bar.webp",
|
"image_path": "images/items/energy_bar.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"mystery_pills": {
|
"mystery_pills": {
|
||||||
"name": "Mystery Pills",
|
"name": {
|
||||||
|
"en": "Mystery Pills",
|
||||||
|
"es": "Píldoras misteriosas"
|
||||||
|
},
|
||||||
"weight": 0.05,
|
"weight": 0.05,
|
||||||
"volume": 0.05,
|
"volume": 0.05,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 30,
|
"hp_restore": 30,
|
||||||
"emoji": "\ud83d\udc8a",
|
"emoji": "💊",
|
||||||
"image_path": "images/items/mystery_pills.webp",
|
"image_path": "images/items/mystery_pills.webp",
|
||||||
"description": "Can be consumed to restore health or stamina."
|
"description": {
|
||||||
|
"en": "Can be consumed to restore health or stamina.",
|
||||||
|
"es": "Se puede consumir para restaurar salud o stamina."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"first_aid_kit": {
|
"first_aid_kit": {
|
||||||
"name": "First Aid Kit",
|
"name": {
|
||||||
"description": "A professional medical kit with bandages, antiseptic, and pain relievers.",
|
"en": "First Aid Kit",
|
||||||
|
"es": "Kit de primeros auxilios"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A professional medical kit with bandages, antiseptic, and pain relievers.",
|
||||||
|
"es": "Un kit médico profesional con vendajes, antisépticos y analgésicos."
|
||||||
|
},
|
||||||
"weight": 0.8,
|
"weight": 0.8,
|
||||||
"volume": 0.5,
|
"volume": 0.5,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 50,
|
"hp_restore": 50,
|
||||||
"emoji": "\ud83e\ude79",
|
"emoji": "🩹",
|
||||||
"image_path": "images/items/first_aid_kit.webp"
|
"image_path": "images/items/first_aid_kit.webp"
|
||||||
},
|
},
|
||||||
"bandage": {
|
"bandage": {
|
||||||
"name": "Bandage",
|
"name": {
|
||||||
"description": "Clean cloth bandages for treating minor wounds. Can stop bleeding.",
|
"en": "Bandage",
|
||||||
|
"es": "Vendaje"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Clean cloth bandages for treating minor wounds. Can stop bleeding.",
|
||||||
|
"es": "Vendajes limpios de tela para tratar heridas menores. Pueden detener la sangrado."
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 15,
|
"hp_restore": 15,
|
||||||
"treats": "Bleeding",
|
"treats": "Bleeding",
|
||||||
"emoji": "\ud83e\ude79",
|
"emoji": "🩹",
|
||||||
"image_path": "images/items/bandage.webp"
|
"image_path": "images/items/bandage.webp"
|
||||||
},
|
},
|
||||||
"medical_supplies": {
|
"medical_supplies": {
|
||||||
"name": "Medical Supplies",
|
"name": {
|
||||||
"description": "Assorted medical supplies scavenged from a clinic.",
|
"en": "Medical Supplies",
|
||||||
|
"es": "Suministros médicos"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Assorted medical supplies scavenged from a clinic.",
|
||||||
|
"es": "Suministros médicos diversos robados de una clínica."
|
||||||
|
},
|
||||||
"weight": 0.6,
|
"weight": 0.6,
|
||||||
"volume": 0.4,
|
"volume": 0.4,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 40,
|
"hp_restore": 40,
|
||||||
"emoji": "\u2695\ufe0f",
|
"emoji": "⚕️",
|
||||||
"image_path": "images/items/medical_supplies.webp"
|
"image_path": "images/items/medical_supplies.webp"
|
||||||
},
|
},
|
||||||
"antibiotics": {
|
"antibiotics": {
|
||||||
"name": "Antibiotics",
|
"name": {
|
||||||
"description": "Pills that fight infections. Expired, but better than nothing.",
|
"en": "Antibiotics",
|
||||||
|
"es": "Antibióticos"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Pills that fight infections. Expired, but better than nothing.",
|
||||||
|
"es": "Píldoras que combaten las infecciones. Caducadas, pero mejor que nada."
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 20,
|
"hp_restore": 20,
|
||||||
"treats": "Infected",
|
"treats": "Infected",
|
||||||
"emoji": "\ud83d\udc8a",
|
"emoji": "💊",
|
||||||
"image_path": "images/items/antibiotics.webp"
|
"image_path": "images/items/antibiotics.webp"
|
||||||
},
|
},
|
||||||
"rad_pills": {
|
"rad_pills": {
|
||||||
"name": "Rad Pills",
|
"name": {
|
||||||
"description": "Anti-radiation medication. Helps flush radioactive particles from the body.",
|
"en": "Rad Pills",
|
||||||
|
"es": "Píldoras de radiación"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Anti-radiation medication. Helps flush radioactive particles from the body.",
|
||||||
|
"es": "Medicamento antirradiación. Ayuda a eliminar partículas radiactivas del cuerpo."
|
||||||
|
},
|
||||||
"weight": 0.05,
|
"weight": 0.05,
|
||||||
"volume": 0.05,
|
"volume": 0.05,
|
||||||
"type": "consumable",
|
"type": "consumable",
|
||||||
"hp_restore": 5,
|
"hp_restore": 5,
|
||||||
"treats": "Radiation",
|
"treats": "Radiation",
|
||||||
"emoji": "\u2622\ufe0f",
|
"emoji": "☢️",
|
||||||
"image_path": "images/items/rad_pills.webp"
|
"image_path": "images/items/rad_pills.webp"
|
||||||
},
|
},
|
||||||
"tire_iron": {
|
"tire_iron": {
|
||||||
"name": "Tire Iron",
|
"name": {
|
||||||
"description": "A heavy metal tool. Makes a decent improvised weapon.",
|
"en": "Tire Iron",
|
||||||
|
"es": "Herramienta de neumático"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A heavy metal tool. Makes a decent improvised weapon.",
|
||||||
|
"es": "Un herramienta de metal pesado. Sirve como un buen arma improvisada."
|
||||||
|
},
|
||||||
"weight": 2.0,
|
"weight": 2.0,
|
||||||
"volume": 1.0,
|
"volume": 1.0,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
@@ -252,17 +396,23 @@
|
|||||||
"damage_min": 3,
|
"damage_min": 3,
|
||||||
"damage_max": 5
|
"damage_max": 5
|
||||||
},
|
},
|
||||||
"emoji": "\ud83d\udd27",
|
"emoji": "🔧",
|
||||||
"image_path": "images/items/tire_iron.webp"
|
"image_path": "images/items/tire_iron.webp"
|
||||||
},
|
},
|
||||||
"baseball_bat": {
|
"baseball_bat": {
|
||||||
"name": "Baseball Bat",
|
"name": {
|
||||||
"description": "Wooden bat with dents and bloodstains. Someone used this before you.",
|
"en": "Baseball Bat",
|
||||||
|
"es": "Bate de béisbol"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Wooden bat with dents and bloodstains. Someone used this before you.",
|
||||||
|
"es": "Bate de béisbol con dientes y manchas de sangre. Alguien lo usó antes que tú."
|
||||||
|
},
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"volume": 1.5,
|
"volume": 1.5,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
"slot": "hand",
|
"slot": "hand",
|
||||||
"emoji": "\u26be",
|
"emoji": "⚾",
|
||||||
"image_path": "images/items/baseball_bat.webp",
|
"image_path": "images/items/baseball_bat.webp",
|
||||||
"stats": {
|
"stats": {
|
||||||
"damage_min": 5,
|
"damage_min": 5,
|
||||||
@@ -270,8 +420,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rusty_knife": {
|
"rusty_knife": {
|
||||||
"name": "Rusty Knife",
|
"name": {
|
||||||
"description": "A dull, rusted blade. Better than your fists.",
|
"en": "Rusty Knife",
|
||||||
|
"es": "Navaja oxidada"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A dull, rusted blade. Better than your fists.",
|
||||||
|
"es": "Una navaja desgastada y oxidada. Mejor que tus puños."
|
||||||
|
},
|
||||||
"weight": 0.3,
|
"weight": 0.3,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
@@ -296,12 +452,18 @@
|
|||||||
"damage_min": 2,
|
"damage_min": 2,
|
||||||
"damage_max": 5
|
"damage_max": 5
|
||||||
},
|
},
|
||||||
"emoji": "\ud83d\udd2a",
|
"emoji": "🔪",
|
||||||
"image_path": "images/items/rusty_knife.webp"
|
"image_path": "images/items/rusty_knife.webp"
|
||||||
},
|
},
|
||||||
"knife": {
|
"knife": {
|
||||||
"name": "Knife",
|
"name": {
|
||||||
"description": "A sharp survival knife in decent condition.",
|
"en": "Knife",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A sharp survival knife in decent condition.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.3,
|
"weight": 0.3,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
@@ -379,17 +541,23 @@
|
|||||||
"duration": 3
|
"duration": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"emoji": "\ud83d\udd2a",
|
"emoji": "🔪",
|
||||||
"image_path": "images/items/knife.webp"
|
"image_path": "images/items/knife.webp"
|
||||||
},
|
},
|
||||||
"rusty_pipe": {
|
"rusty_pipe": {
|
||||||
"name": "Rusty Pipe",
|
"name": {
|
||||||
"description": "Heavy metal pipe. Crude but effective.",
|
"en": "Rusty Pipe",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Heavy metal pipe. Crude but effective.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.5,
|
"weight": 1.5,
|
||||||
"volume": 0.8,
|
"volume": 0.8,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
"slot": "hand",
|
"slot": "hand",
|
||||||
"emoji": "\ud83d\udd29",
|
"emoji": "🔩",
|
||||||
"image_path": "images/items/rusty_pipe.webp",
|
"image_path": "images/items/rusty_pipe.webp",
|
||||||
"stats": {
|
"stats": {
|
||||||
"damage_min": 5,
|
"damage_min": 5,
|
||||||
@@ -397,8 +565,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tattered_rucksack": {
|
"tattered_rucksack": {
|
||||||
"name": "Tattered Rucksack",
|
"name": {
|
||||||
"description": "An old backpack with torn straps. Still functional.",
|
"en": "Tattered Rucksack",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "An old backpack with torn straps. Still functional.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"volume": 0.5,
|
"volume": 0.5,
|
||||||
"type": "backpack",
|
"type": "backpack",
|
||||||
@@ -434,12 +608,18 @@
|
|||||||
"weight_capacity": 10,
|
"weight_capacity": 10,
|
||||||
"volume_capacity": 10
|
"volume_capacity": 10
|
||||||
},
|
},
|
||||||
"emoji": "\ud83c\udf92",
|
"emoji": "🎒",
|
||||||
"image_path": "images/items/tattered_rucksack.webp"
|
"image_path": "images/items/tattered_rucksack.webp"
|
||||||
},
|
},
|
||||||
"hiking_backpack": {
|
"hiking_backpack": {
|
||||||
"name": "Hiking Backpack",
|
"name": {
|
||||||
"description": "A quality backpack with multiple compartments.",
|
"en": "Hiking Backpack",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A quality backpack with multiple compartments.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.5,
|
"weight": 1.5,
|
||||||
"volume": 0.7,
|
"volume": 0.7,
|
||||||
"type": "backpack",
|
"type": "backpack",
|
||||||
@@ -464,17 +644,23 @@
|
|||||||
"weight_capacity": 20,
|
"weight_capacity": 20,
|
||||||
"volume_capacity": 20
|
"volume_capacity": 20
|
||||||
},
|
},
|
||||||
"emoji": "\ud83c\udf92",
|
"emoji": "🎒",
|
||||||
"image_path": "images/items/hiking_backpack.webp"
|
"image_path": "images/items/hiking_backpack.webp"
|
||||||
},
|
},
|
||||||
"flashlight": {
|
"flashlight": {
|
||||||
"name": "Flashlight",
|
"name": {
|
||||||
"description": "A battery-powered flashlight. Batteries low but working.",
|
"en": "Flashlight",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A battery-powered flashlight. Batteries low but working.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.3,
|
"weight": 0.3,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "tool",
|
"type": "tool",
|
||||||
"slot": "tool",
|
"slot": "tool",
|
||||||
"emoji": "\ud83d\udd26",
|
"emoji": "🔦",
|
||||||
"image_path": "images/items/flashlight.webp",
|
"image_path": "images/items/flashlight.webp",
|
||||||
"stats": {
|
"stats": {
|
||||||
"damage_min": 5,
|
"damage_min": 5,
|
||||||
@@ -482,26 +668,44 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"old_photograph": {
|
"old_photograph": {
|
||||||
"name": "Old Photograph",
|
"name": {
|
||||||
|
"en": "Old Photograph",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.01,
|
"weight": 0.01,
|
||||||
"volume": 0.01,
|
"volume": 0.01,
|
||||||
"type": "quest",
|
"type": "quest",
|
||||||
"emoji": "\ud83d\udcf7",
|
"emoji": "📷",
|
||||||
"image_path": "images/items/old_photograph.webp",
|
"image_path": "images/items/old_photograph.webp",
|
||||||
"description": "A useful old photograph."
|
"description": {
|
||||||
|
"en": "A useful old photograph.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"key_ring": {
|
"key_ring": {
|
||||||
"name": "Key Ring",
|
"name": {
|
||||||
|
"en": "Key Ring",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.05,
|
"volume": 0.05,
|
||||||
"type": "quest",
|
"type": "quest",
|
||||||
"emoji": "\ud83d\udd11",
|
"emoji": "🔑",
|
||||||
"image_path": "images/items/key_ring.webp",
|
"image_path": "images/items/key_ring.webp",
|
||||||
"description": "A useful key ring."
|
"description": {
|
||||||
|
"en": "A useful key ring.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"makeshift_spear": {
|
"makeshift_spear": {
|
||||||
"name": "Makeshift Spear",
|
"name": {
|
||||||
"description": "A crude spear made from a sharpened stick and scrap metal.",
|
"en": "Makeshift Spear",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A crude spear made from a sharpened stick and scrap metal.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.2,
|
"weight": 1.2,
|
||||||
"volume": 2.0,
|
"volume": 2.0,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
@@ -541,12 +745,18 @@
|
|||||||
"damage_min": 4,
|
"damage_min": 4,
|
||||||
"damage_max": 7
|
"damage_max": 7
|
||||||
},
|
},
|
||||||
"emoji": "\u2694\ufe0f",
|
"emoji": "⚔️",
|
||||||
"image_path": "images/items/makeshift_spear.webp"
|
"image_path": "images/items/makeshift_spear.webp"
|
||||||
},
|
},
|
||||||
"reinforced_bat": {
|
"reinforced_bat": {
|
||||||
"name": "Reinforced Bat",
|
"name": {
|
||||||
"description": "A wooden bat wrapped with scrap metal and nails. Brutal.",
|
"en": "Reinforced Bat",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A wooden bat wrapped with scrap metal and nails. Brutal.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.8,
|
"weight": 1.8,
|
||||||
"volume": 1.5,
|
"volume": 1.5,
|
||||||
"type": "weapon",
|
"type": "weapon",
|
||||||
@@ -592,12 +802,18 @@
|
|||||||
"duration": 1
|
"duration": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"emoji": "\ud83c\udff8",
|
"emoji": "🏸",
|
||||||
"image_path": "images/items/reinforced_bat.webp"
|
"image_path": "images/items/reinforced_bat.webp"
|
||||||
},
|
},
|
||||||
"leather_vest": {
|
"leather_vest": {
|
||||||
"name": "Leather Vest",
|
"name": {
|
||||||
"description": "A makeshift vest crafted from leather scraps. Provides basic protection.",
|
"en": "Leather Vest",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A makeshift vest crafted from leather scraps. Provides basic protection.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.5,
|
"weight": 1.5,
|
||||||
"volume": 1.0,
|
"volume": 1.0,
|
||||||
"type": "armor",
|
"type": "armor",
|
||||||
@@ -637,12 +853,18 @@
|
|||||||
"armor": 3,
|
"armor": 3,
|
||||||
"hp_bonus": 10
|
"hp_bonus": 10
|
||||||
},
|
},
|
||||||
"emoji": "\ud83e\uddba",
|
"emoji": "🦺",
|
||||||
"image_path": "images/items/leather_vest.webp"
|
"image_path": "images/items/leather_vest.webp"
|
||||||
},
|
},
|
||||||
"cloth_bandana": {
|
"cloth_bandana": {
|
||||||
"name": "Cloth Bandana",
|
"name": {
|
||||||
"description": "A simple cloth head covering. Keeps the sun and dust out.",
|
"en": "Cloth Bandana",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A simple cloth head covering. Keeps the sun and dust out.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
"volume": 0.1,
|
"volume": 0.1,
|
||||||
"type": "clothing",
|
"type": "clothing",
|
||||||
@@ -669,12 +891,18 @@
|
|||||||
"stats": {
|
"stats": {
|
||||||
"armor": 1
|
"armor": 1
|
||||||
},
|
},
|
||||||
"emoji": "\ud83e\udde3",
|
"emoji": "🧣",
|
||||||
"image_path": "images/items/cloth_bandana.webp"
|
"image_path": "images/items/cloth_bandana.webp"
|
||||||
},
|
},
|
||||||
"sturdy_boots": {
|
"sturdy_boots": {
|
||||||
"name": "Sturdy Boots",
|
"name": {
|
||||||
"description": "Reinforced boots for traversing the wasteland.",
|
"en": "Sturdy Boots",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Reinforced boots for traversing the wasteland.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"volume": 0.8,
|
"volume": 0.8,
|
||||||
"type": "clothing",
|
"type": "clothing",
|
||||||
@@ -714,12 +942,18 @@
|
|||||||
"armor": 2,
|
"armor": 2,
|
||||||
"stamina_bonus": 5
|
"stamina_bonus": 5
|
||||||
},
|
},
|
||||||
"emoji": "\ud83e\udd7e",
|
"emoji": "🥾",
|
||||||
"image_path": "images/items/sturdy_boots.webp"
|
"image_path": "images/items/sturdy_boots.webp"
|
||||||
},
|
},
|
||||||
"padded_pants": {
|
"padded_pants": {
|
||||||
"name": "Padded Pants",
|
"name": {
|
||||||
"description": "Pants reinforced with extra padding for protection.",
|
"en": "Padded Pants",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Pants reinforced with extra padding for protection.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.8,
|
"weight": 0.8,
|
||||||
"volume": 0.6,
|
"volume": 0.6,
|
||||||
"type": "armor",
|
"type": "armor",
|
||||||
@@ -755,12 +989,18 @@
|
|||||||
"armor": 2,
|
"armor": 2,
|
||||||
"hp_bonus": 5
|
"hp_bonus": 5
|
||||||
},
|
},
|
||||||
"emoji": "\ud83d\udc56",
|
"emoji": "👖",
|
||||||
"image_path": "images/items/padded_pants.webp"
|
"image_path": "images/items/padded_pants.webp"
|
||||||
},
|
},
|
||||||
"reinforced_pack": {
|
"reinforced_pack": {
|
||||||
"name": "Reinforced Pack",
|
"name": {
|
||||||
"description": "A custom-built backpack with metal frame and extra pockets.",
|
"en": "Reinforced Pack",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A custom-built backpack with metal frame and extra pockets.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 2.0,
|
"weight": 2.0,
|
||||||
"volume": 0.9,
|
"volume": 0.9,
|
||||||
"type": "backpack",
|
"type": "backpack",
|
||||||
@@ -839,12 +1079,18 @@
|
|||||||
"weight_capacity": 30,
|
"weight_capacity": 30,
|
||||||
"volume_capacity": 30
|
"volume_capacity": 30
|
||||||
},
|
},
|
||||||
"emoji": "\ud83c\udf92",
|
"emoji": "🎒",
|
||||||
"image_path": "images/items/reinforced_pack.webp"
|
"image_path": "images/items/reinforced_pack.webp"
|
||||||
},
|
},
|
||||||
"hammer": {
|
"hammer": {
|
||||||
"name": "Hammer",
|
"name": {
|
||||||
"description": "A basic tool for crafting and repairs. Essential for any survivor.",
|
"en": "Hammer",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A basic tool for crafting and repairs. Essential for any survivor.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.8,
|
"weight": 0.8,
|
||||||
"volume": 0.4,
|
"volume": 0.4,
|
||||||
"type": "tool",
|
"type": "tool",
|
||||||
@@ -872,12 +1118,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"repair_percentage": 30,
|
"repair_percentage": 30,
|
||||||
"emoji": "\ud83d\udd28",
|
"emoji": "🔨",
|
||||||
"image_path": "images/items/hammer.webp"
|
"image_path": "images/items/hammer.webp"
|
||||||
},
|
},
|
||||||
"screwdriver": {
|
"screwdriver": {
|
||||||
"name": "Screwdriver",
|
"name": {
|
||||||
"description": "A flathead screwdriver. Useful for repairs and scavenging.",
|
"en": "Screwdriver",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A flathead screwdriver. Useful for repairs and scavenging.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"weight": 0.2,
|
"weight": 0.2,
|
||||||
"volume": 0.2,
|
"volume": 0.2,
|
||||||
"type": "tool",
|
"type": "tool",
|
||||||
@@ -905,7 +1157,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"repair_percentage": 25,
|
"repair_percentage": 25,
|
||||||
"emoji": "\ud83e\ude9b",
|
"emoji": "🪛",
|
||||||
"image_path": "images/items/screwdriver.webp",
|
"image_path": "images/items/screwdriver.webp",
|
||||||
"stats": {
|
"stats": {
|
||||||
"damage_min": 5,
|
"damage_min": 5,
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
"locations": [
|
"locations": [
|
||||||
{
|
{
|
||||||
"id": "start_point",
|
"id": "start_point",
|
||||||
"name": "\ud83c\udf06 Ruined Downtown Core",
|
"name": {
|
||||||
"description": "The wind howls through skeletal skyscrapers. Debris litters the cracked asphalt. You sense danger, but also opportunity.",
|
"en": "🌆 Ruined Downtown Core",
|
||||||
|
"es": "🌆 Centro de la ciudad destruido"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "The wind howls through skeletal skyscrapers. Debris litters the cracked asphalt. You sense danger, but also opportunity.",
|
||||||
|
"es": "El viento ruge a través de los esqueléticos rascacielos. El desastre llena el asfalto roto. Sientes el peligro, pero también la oportunidad."
|
||||||
|
},
|
||||||
"image_path": "images/locations/downtown.webp",
|
"image_path": "images/locations/downtown.webp",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -33,10 +39,19 @@
|
|||||||
"stamina_cost": 2,
|
"stamina_cost": 2,
|
||||||
"success_rate": 0.5,
|
"success_rate": 0.5,
|
||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "You disturb a nest of rats! They bite you! (-8 HP)",
|
"crit_failure": {
|
||||||
|
"en": "You disturb a nest of rats! They bite you!",
|
||||||
|
"es": "Te topas con una colmena de ratones. Te muerden!"
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "Just rotting garbage. Nothing useful.",
|
"failure": {
|
||||||
"success": "Despite the smell, you find some [Plastic Bottles] and [Cloth Scraps]."
|
"en": "Just rotting garbage. Nothing useful.",
|
||||||
|
"es": "Solo escombros rotos. Nada útil."
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "Despite the smell, you find some [Plastic Bottles] and [Cloth Scraps].",
|
||||||
|
"es": "A pesar del olor, encuentras algunos [Botellas de plástico] y [Ramas de tela]."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,8 +79,14 @@
|
|||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "",
|
"crit_failure": "",
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "The trunk is rusted shut. You can't get it open.",
|
"failure": {
|
||||||
"success": "With a great heave, you pry the trunk open and find a [Tire Iron]!"
|
"en": "The trunk is rusted shut. You can't get it open.",
|
||||||
|
"es": "El maletero está oxidado. No puedes abrirlo."
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
|
||||||
|
"es": "Con un gran esfuerzo, pruebas el maletero y encuentras una [Herramienta de neumáticos]!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_glovebox": {
|
"search_glovebox": {
|
||||||
@@ -88,8 +109,14 @@
|
|||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "",
|
"crit_failure": "",
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "The glovebox is empty except for dust and old receipts.",
|
"failure": {
|
||||||
"success": "You find a half-eaten [Stale Chocolate Bar]."
|
"en": "The glovebox is empty except for dust and old receipts.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You find a half-eaten [Stale Chocolate Bar].",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,8 +126,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gas_station",
|
"id": "gas_station",
|
||||||
"name": "\u26fd\ufe0f Abandoned Gas Station",
|
"name": {
|
||||||
"description": "The smell of stale gasoline hangs in the air. A rusty sedan sits by the pumps, its door ajar. Behind the station, you spot a small tool shed with a workbench.",
|
"en": "⛽️ Abandoned Gas Station",
|
||||||
|
"es": "⛽️ Gasolinera abandonada"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "The smell of stale gasoline hangs in the air. A rusty sedan sits by the pumps, its door ajar. Behind the station, you spot a small tool shed with a workbench.",
|
||||||
|
"es": "El olor a gasolina se suspende en el aire. Un sedán oxidado está en los surtidores, su puerta está abierta. Por detrás de la gasolinera, ves un pequeño almacén de herramientas con una mesa de trabajo."
|
||||||
|
},
|
||||||
"image_path": "images/locations/gas_station.webp",
|
"image_path": "images/locations/gas_station.webp",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@@ -141,10 +174,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find some cloth scraps and plastic in the glovebox.",
|
"success": {
|
||||||
"failure": "The glovebox is empty except for old papers.",
|
"en": "You find some cloth scraps and plastic in the glovebox.",
|
||||||
"crit_success": "You find scrap metal from the dashboard!",
|
"es": ""
|
||||||
"crit_failure": "The glovebox is jammed shut."
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The glovebox is empty except for old papers.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "You find scrap metal from the dashboard!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "The glovebox is jammed shut.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pop_trunk": {
|
"pop_trunk": {
|
||||||
@@ -176,10 +221,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You force the trunk open and find scrap metal and plastic.",
|
"success": {
|
||||||
"failure": "The trunk is rusted shut.",
|
"en": "You force the trunk open and find scrap metal and plastic.",
|
||||||
"crit_success": "The trunk contains tools!",
|
"es": ""
|
||||||
"crit_failure": "You cut your hand on rusty metal! (-5 HP)"
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The trunk is rusted shut.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "The trunk contains tools!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "You cut your hand on rusty metal! (-5 HP)",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,10 +273,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find scrap metal and cloth in the storage box.",
|
"success": {
|
||||||
"failure": "The storage box is mostly empty.",
|
"en": "You find scrap metal and cloth in the storage box.",
|
||||||
"crit_success": "You discover tools inside!",
|
"es": ""
|
||||||
"crit_failure": "Just oil stains and rust."
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The storage box is mostly empty.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "You discover tools inside!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "Just oil stains and rust.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,8 +297,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "residential",
|
"id": "residential",
|
||||||
"name": "\ud83c\udfd8\ufe0f Residential Street",
|
"name": {
|
||||||
"description": "A quiet suburban street lined with abandoned homes. Most are boarded up, but a few doors hang open, creaking in the wind.",
|
"en": "🏘️ Residential Street",
|
||||||
|
"es": "🏘️ Calle residencial"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A quiet suburban street lined with abandoned homes. Most are boarded up, but a few doors hang open, creaking in the wind.",
|
||||||
|
"es": "Una tranquila calle suburbana llena de casas abandonadas. La mayoría están tapiadas, pero algunas puertas están abiertas, movidas por el viento."
|
||||||
|
},
|
||||||
"image_path": "images/locations/residential.webp",
|
"image_path": "images/locations/residential.webp",
|
||||||
"x": 3,
|
"x": 3,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -264,10 +339,19 @@
|
|||||||
"stamina_cost": 3,
|
"stamina_cost": 3,
|
||||||
"success_rate": 0.5,
|
"success_rate": 0.5,
|
||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "The floor collapses beneath you! (-10 HP)",
|
"crit_failure": {
|
||||||
|
"en": "The floor collapses beneath you! (-10 HP)",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "The house has already been thoroughly looted. Nothing remains.",
|
"failure": {
|
||||||
"success": "You find some useful supplies: [Canned Beans], [Bottled Water], and [Cloth Scraps]!"
|
"en": "The house has already been thoroughly looted. Nothing remains.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You find some useful supplies: [Canned Beans], [Bottled Water], and [Cloth Scraps]!",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -277,8 +361,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "clinic",
|
"id": "clinic",
|
||||||
"name": "\ud83c\udfe5 Old Clinic",
|
"name": {
|
||||||
"description": "A small medical clinic, its windows shattered. The waiting room is a mess of overturned chairs and scattered papers. The examination rooms might still have supplies.",
|
"en": "🏥 Old Clinic",
|
||||||
|
"es": "🏥 Clínica abandonada"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A small medical clinic, its windows shattered. The waiting room is a mess of overturned chairs and scattered papers. The examination rooms might still have supplies.",
|
||||||
|
"es": "Una pequeña clínica médica, sus ventanas están rotas. El salón de espera es un desastre de sillas invertidas y papeles dispersos. Las habitaciones de examen pueden todavía tener suministros."
|
||||||
|
},
|
||||||
"image_path": "images/locations/clinic.webp",
|
"image_path": "images/locations/clinic.webp",
|
||||||
"x": 2,
|
"x": 2,
|
||||||
"y": 3,
|
"y": 3,
|
||||||
@@ -310,8 +400,14 @@
|
|||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "",
|
"crit_failure": "",
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "The cabinet is empty. Someone got here first.",
|
"failure": {
|
||||||
"success": "Jackpot! You find a [First Aid Kit] and some [Bandages]!"
|
"en": "The cabinet is empty. Someone got here first.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "Jackpot! You find a [First Aid Kit] and some [Bandages]!",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -321,8 +417,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "plaza",
|
"id": "plaza",
|
||||||
"name": "\ud83c\udfec Shopping Plaza",
|
"name": {
|
||||||
"description": "A strip mall with broken storefronts. Most shops have been thoroughly ransacked, but you might find something if you search carefully.",
|
"en": "🏬 Shopping Plaza",
|
||||||
|
"es": "🏬 Plaza de comercio"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A strip mall with broken storefronts. Most shops have been thoroughly ransacked, but you might find something if you search carefully.",
|
||||||
|
"es": "Una plaza de comercio con vitrinas rotas. La mayoría de las tiendas han sido despojadas, pero puedes encontrar algo si buscas con cuidado."
|
||||||
|
},
|
||||||
"image_path": "images/locations/plaza.webp",
|
"image_path": "images/locations/plaza.webp",
|
||||||
"x": -2.5,
|
"x": -2.5,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -359,10 +461,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You smash the vending machine and grab bottles and scrap.",
|
"success": {
|
||||||
"failure": "The machine is too sturdy to break.",
|
"en": "You smash the vending machine and grab bottles and scrap.",
|
||||||
"crit_success": "Packaged food falls out!",
|
"es": ""
|
||||||
"crit_failure": "Glass cuts your arm! (-10 HP)"
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The machine is too sturdy to break.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "Packaged food falls out!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "Glass cuts your arm! (-10 HP)",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -389,10 +503,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find a plastic bottle at the bottom.",
|
"success": {
|
||||||
"failure": "Nothing left to scavenge.",
|
"en": "You find a plastic bottle at the bottom.",
|
||||||
"crit_success": "A snack is wedged in the dispenser!",
|
"es": ""
|
||||||
"crit_failure": "Already picked clean."
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Nothing left to scavenge.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "A snack is wedged in the dispenser!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "Already picked clean.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,10 +555,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You dig through rubble and find scrap metal and cloth.",
|
"success": {
|
||||||
"failure": "Just broken concrete and dust.",
|
"en": "You dig through rubble and find scrap metal and cloth.",
|
||||||
"crit_success": "A tool was buried in the debris!",
|
"es": ""
|
||||||
"crit_failure": "Sharp debris cuts you! (-5 HP)"
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Just broken concrete and dust.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "A tool was buried in the debris!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "Sharp debris cuts you! (-5 HP)",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,8 +579,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "park",
|
"id": "park",
|
||||||
"name": "\ud83c\udf33 Suburban Park",
|
"name": {
|
||||||
"description": "An overgrown park with rusted playground equipment. Nature is slowly reclaiming this space. A maintenance shed sits at the far end.",
|
"en": "🌳 Suburban Park",
|
||||||
|
"es": "🌳 Parque suburbano"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "An overgrown park with rusted playground equipment. Nature is slowly reclaiming this space. A maintenance shed sits at the far end.",
|
||||||
|
"es": "Un parque suburbano deshabitado con equipos de juegos oxidados. La naturaleza está reclamando este espacio. Un almacén de mantenimiento se encuentra al final."
|
||||||
|
},
|
||||||
"image_path": "images/locations/park.webp",
|
"image_path": "images/locations/park.webp",
|
||||||
"x": -1,
|
"x": -1,
|
||||||
"y": -2,
|
"y": -2,
|
||||||
@@ -484,8 +628,14 @@
|
|||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "",
|
"crit_failure": "",
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "The shed has been picked clean. Only empty shelves remain.",
|
"failure": {
|
||||||
"success": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!"
|
"en": "The shed has been picked clean. Only empty shelves remain.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -495,8 +645,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "overpass",
|
"id": "overpass",
|
||||||
"name": "\ud83d\udee3\ufe0f Highway Overpass",
|
"name": {
|
||||||
"description": "A concrete overpass spanning the cracked highway below. Abandoned vehicles litter the road. This is a good vantage point to survey the area.",
|
"en": "🛣️ Highway Overpass",
|
||||||
|
"es": "🛣️ Puesto de carretera"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A concrete overpass spanning the cracked highway below. Abandoned vehicles litter the road. This is a good vantage point to survey the area.",
|
||||||
|
"es": "Un puesto de carretera de cemento que atraviesa la carretera rota por debajo. Vehículos abandonados se desvanecen por la carretera. Este es un buen punto de vista para examinar el área."
|
||||||
|
},
|
||||||
"x": 1.0,
|
"x": 1.0,
|
||||||
"y": 4.5,
|
"y": 4.5,
|
||||||
"image_path": "images/locations/overpass.webp",
|
"image_path": "images/locations/overpass.webp",
|
||||||
@@ -510,8 +666,14 @@
|
|||||||
"crit_success_chance": 0.1,
|
"crit_success_chance": 0.1,
|
||||||
"crit_failure_chance": 0.1,
|
"crit_failure_chance": 0.1,
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find a half-eaten [Stale Chocolate Bar].",
|
"success": {
|
||||||
"failure": "The glovebox is empty except for dust and old receipts.",
|
"en": "You find a half-eaten [Stale Chocolate Bar].",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The glovebox is empty except for dust and old receipts.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"crit_failure": ""
|
"crit_failure": ""
|
||||||
},
|
},
|
||||||
@@ -534,8 +696,14 @@
|
|||||||
"crit_success_chance": 0.1,
|
"crit_success_chance": 0.1,
|
||||||
"crit_failure_chance": 0.1,
|
"crit_failure_chance": 0.1,
|
||||||
"text": {
|
"text": {
|
||||||
"success": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
|
"success": {
|
||||||
"failure": "The trunk is rusted shut. You can't get it open.",
|
"en": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The trunk is rusted shut. You can't get it open.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"crit_failure": ""
|
"crit_failure": ""
|
||||||
},
|
},
|
||||||
@@ -563,8 +731,14 @@
|
|||||||
"crit_success_chance": 0.1,
|
"crit_success_chance": 0.1,
|
||||||
"crit_failure_chance": 0.1,
|
"crit_failure_chance": 0.1,
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find a half-eaten [Stale Chocolate Bar].",
|
"success": {
|
||||||
"failure": "The glovebox is empty except for dust and old receipts.",
|
"en": "You find a half-eaten [Stale Chocolate Bar].",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The glovebox is empty except for dust and old receipts.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"crit_failure": ""
|
"crit_failure": ""
|
||||||
},
|
},
|
||||||
@@ -587,8 +761,14 @@
|
|||||||
"crit_success_chance": 0.1,
|
"crit_success_chance": 0.1,
|
||||||
"crit_failure_chance": 0.1,
|
"crit_failure_chance": 0.1,
|
||||||
"text": {
|
"text": {
|
||||||
"success": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
|
"success": {
|
||||||
"failure": "The trunk is rusted shut. You can't get it open.",
|
"en": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The trunk is rusted shut. You can't get it open.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"crit_failure": ""
|
"crit_failure": ""
|
||||||
},
|
},
|
||||||
@@ -611,8 +791,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "warehouse",
|
"id": "warehouse",
|
||||||
"name": "\ud83c\udfed Warehouse District",
|
"name": {
|
||||||
"description": "Rows of industrial warehouses stretch before you. Metal doors creak in the wind. The loading docks are littered with debris and abandoned cargo.",
|
"en": "🏭 Warehouse District",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Rows of industrial warehouses stretch before you. Metal doors creak in the wind. The loading docks are littered with debris and abandoned cargo.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/warehouse.webp",
|
"image_path": "images/locations/warehouse.webp",
|
||||||
"x": 4,
|
"x": 4,
|
||||||
"y": -1.5,
|
"y": -1.5,
|
||||||
@@ -642,10 +828,19 @@
|
|||||||
"stamina_cost": 2,
|
"stamina_cost": 2,
|
||||||
"success_rate": 0.5,
|
"success_rate": 0.5,
|
||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "You disturb a nest of rats! They bite you! (-8 HP)",
|
"crit_failure": {
|
||||||
|
"en": "You disturb a nest of rats! They bite you! (-8 HP)",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "Just rotting garbage. Nothing useful.",
|
"failure": {
|
||||||
"success": "Despite the smell, you find some [Plastic Bottles] and [Cloth Scraps]."
|
"en": "Just rotting garbage. Nothing useful.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "Despite the smell, you find some [Plastic Bottles] and [Cloth Scraps].",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -683,8 +878,14 @@
|
|||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "",
|
"crit_failure": "",
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"failure": "The shed has been picked clean. Only empty shelves remain.",
|
"failure": {
|
||||||
"success": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!"
|
"en": "The shed has been picked clean. Only empty shelves remain.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -694,8 +895,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "warehouse_interior",
|
"id": "warehouse_interior",
|
||||||
"name": "\ud83d\udce6 Warehouse Interior",
|
"name": {
|
||||||
"description": "Inside the warehouse, towering shelves cast long shadows. Scattered crates and pallets suggest this was once a distribution center. The back office door hangs open.",
|
"en": "📦 Warehouse Interior",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Inside the warehouse, towering shelves cast long shadows. Scattered crates and pallets suggest this was once a distribution center. The back office door hangs open.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/warehouse_interior.webp",
|
"image_path": "images/locations/warehouse_interior.webp",
|
||||||
"x": 4.5,
|
"x": 4.5,
|
||||||
"y": -2,
|
"y": -2,
|
||||||
@@ -709,8 +916,14 @@
|
|||||||
"crit_success_chance": 0,
|
"crit_success_chance": 0,
|
||||||
"crit_failure_chance": 0,
|
"crit_failure_chance": 0,
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You successfully \ud83d\udd0e search box.",
|
"success": {
|
||||||
"failure": "You failed to \ud83d\udd0e search box.",
|
"en": "You successfully 🔎 search box.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "You failed to 🔎 search box.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"crit_success": "",
|
"crit_success": "",
|
||||||
"crit_failure": ""
|
"crit_failure": ""
|
||||||
},
|
},
|
||||||
@@ -738,8 +951,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "subway",
|
"id": "subway",
|
||||||
"name": "\ud83d\ude87 Subway Station Entrance",
|
"name": {
|
||||||
"description": "Stairs descend into darkness. The entrance to an abandoned subway station yawns before you. Emergency lighting flickers somewhere below.",
|
"en": "🚇 Subway Station Entrance",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Stairs descend into darkness. The entrance to an abandoned subway station yawns before you. Emergency lighting flickers somewhere below.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/subway.webp",
|
"image_path": "images/locations/subway.webp",
|
||||||
"x": -4,
|
"x": -4,
|
||||||
"y": -0.5,
|
"y": -0.5,
|
||||||
@@ -775,10 +994,22 @@
|
|||||||
"stamina_cost": 2,
|
"stamina_cost": 2,
|
||||||
"success_rate": 0.55,
|
"success_rate": 0.55,
|
||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "Debris shifts and hits your leg! (-4 HP)",
|
"crit_failure": {
|
||||||
"crit_success": "You uncover a tool buried deep!",
|
"en": "Debris shifts and hits your leg! (-4 HP)",
|
||||||
"failure": "Just concrete chunks.",
|
"es": ""
|
||||||
"success": "You sift through rubble and find scrap metal."
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "You uncover a tool buried deep!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Just concrete chunks.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You sift through rubble and find scrap metal.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -810,10 +1041,22 @@
|
|||||||
"stamina_cost": 5,
|
"stamina_cost": 5,
|
||||||
"success_rate": 0.6,
|
"success_rate": 0.6,
|
||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "The machine topples on you! (-12 HP)",
|
"crit_failure": {
|
||||||
"crit_success": "Food packages tumble out!",
|
"en": "The machine topples on you! (-12 HP)",
|
||||||
"failure": "The machine won't budge.",
|
"es": ""
|
||||||
"success": "You bash open the vending machine and grab bottles."
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "Food packages tumble out!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "The machine won't budge.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You bash open the vending machine and grab bottles.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -834,10 +1077,22 @@
|
|||||||
"stamina_cost": 2,
|
"stamina_cost": 2,
|
||||||
"success_rate": 0.4,
|
"success_rate": 0.4,
|
||||||
"text": {
|
"text": {
|
||||||
"crit_failure": "Nothing here.",
|
"crit_failure": {
|
||||||
"crit_success": "A bottle still rolls out!",
|
"en": "Nothing here.",
|
||||||
"failure": "Completely empty.",
|
"es": ""
|
||||||
"success": "You find a bottle in the machine's slot."
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "A bottle still rolls out!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Completely empty.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"en": "You find a bottle in the machine's slot.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -847,8 +1102,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "subway_tunnels",
|
"id": "subway_tunnels",
|
||||||
"name": "\ud83d\ude8a Subway Tunnels",
|
"name": {
|
||||||
"description": "Dark subway tunnels stretch into blackness. Flickering emergency lights cast eerie shadows. The third rail is dead, but you should still watch your step.",
|
"en": "🚊 Subway Tunnels",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Dark subway tunnels stretch into blackness. Flickering emergency lights cast eerie shadows. The third rail is dead, but you should still watch your step.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/subway_tunnels.webp",
|
"image_path": "images/locations/subway_tunnels.webp",
|
||||||
"x": -4.5,
|
"x": -4.5,
|
||||||
"y": -1,
|
"y": -1,
|
||||||
@@ -880,10 +1141,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find scrap metal in the tunnel debris.",
|
"success": {
|
||||||
"failure": "Just rocks and dirt.",
|
"en": "You find scrap metal in the tunnel debris.",
|
||||||
"crit_success": "A maintenance tool was left behind!",
|
"es": ""
|
||||||
"crit_failure": "You stumble and hit the wall! (-6 HP)"
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Just rocks and dirt.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "A maintenance tool was left behind!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "You stumble and hit the wall! (-6 HP)",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -892,8 +1165,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "office_building",
|
"id": "office_building",
|
||||||
"name": "\ud83c\udfe2 Office Building",
|
"name": {
|
||||||
"description": "A five-story office building with shattered windows. The lobby is trashed, but the stairs appear intact. You can hear the wind whistling through the upper floors.",
|
"en": "🏢 Office Building",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A five-story office building with shattered windows. The lobby is trashed, but the stairs appear intact. You can hear the wind whistling through the upper floors.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/office_building.webp",
|
"image_path": "images/locations/office_building.webp",
|
||||||
"x": 3.5,
|
"x": 3.5,
|
||||||
"y": 4,
|
"y": 4,
|
||||||
@@ -924,10 +1203,22 @@
|
|||||||
"crit_items": []
|
"crit_items": []
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find scrap metal and cloth in the lobby debris.",
|
"success": {
|
||||||
"failure": "Just broken furniture and papers.",
|
"en": "You find scrap metal and cloth in the lobby debris.",
|
||||||
"crit_success": "You discover useful materials!",
|
"es": ""
|
||||||
"crit_failure": "Glass cuts your hand! (-5 HP)"
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Just broken furniture and papers.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "You discover useful materials!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "Glass cuts your hand! (-5 HP)",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -936,8 +1227,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "office_interior",
|
"id": "office_interior",
|
||||||
"name": "\ud83d\udcbc Office Floors",
|
"name": {
|
||||||
"description": "Cubicles stretch across the floor. Papers scatter in the breeze from broken windows. Filing cabinets stand open, already ransacked. A corner office looks promising.",
|
"en": "💼 Office Floors",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Cubicles stretch across the floor. Papers scatter in the breeze from broken windows. Filing cabinets stand open, already ransacked. A corner office looks promising.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/office_interior.webp",
|
"image_path": "images/locations/office_interior.webp",
|
||||||
"x": 4,
|
"x": 4,
|
||||||
"y": 4.5,
|
"y": 4.5,
|
||||||
@@ -974,10 +1271,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You find cloth and bottles in desk drawers.",
|
"success": {
|
||||||
"failure": "Everything's been picked through already.",
|
"en": "You find cloth and bottles in desk drawers.",
|
||||||
"crit_success": "Someone left food in their desk!",
|
"es": ""
|
||||||
"crit_failure": "Just old paperwork."
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Everything's been picked through already.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "Someone left food in their desk!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "Just old paperwork.",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -986,8 +1295,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "location_1760791397492",
|
"id": "location_1760791397492",
|
||||||
"name": "Subway Section A",
|
"name": {
|
||||||
"description": "A shady dimly lit subway section. All you can see are abandoned train tracks and some garbage lying around. ",
|
"en": "Subway Section A",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A shady dimly lit subway section. All you can see are abandoned train tracks and some garbage lying around. ",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"image_path": "images/locations/subway_section_a.jpg",
|
"image_path": "images/locations/subway_section_a.jpg",
|
||||||
"x": -5,
|
"x": -5,
|
||||||
"y": -2,
|
"y": -2,
|
||||||
@@ -1019,10 +1334,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"success": "You dig through the garbage and find scrap metal.",
|
"success": {
|
||||||
"failure": "Just rotting trash.",
|
"en": "You dig through the garbage and find scrap metal.",
|
||||||
"crit_success": "A tool was discarded here!",
|
"es": ""
|
||||||
"crit_failure": "You step on sharp debris! (-5 HP)"
|
},
|
||||||
|
"failure": {
|
||||||
|
"en": "Just rotting trash.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_success": {
|
||||||
|
"en": "A tool was discarded here!",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"crit_failure": {
|
||||||
|
"en": "You step on sharp debris! (-5 HP)",
|
||||||
|
"es": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
"npcs": {
|
"npcs": {
|
||||||
"feral_dog": {
|
"feral_dog": {
|
||||||
"npc_id": "feral_dog",
|
"npc_id": "feral_dog",
|
||||||
"name": "Feral Dog",
|
"name": {
|
||||||
"description": "A wild, mangy dog with desperate hunger in its eyes. Its ribs are visible beneath matted fur.",
|
"en": "Feral Dog",
|
||||||
|
"es": "Perro feroz"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A wild, mangy dog with desperate hunger in its eyes. Its ribs are visible beneath matted fur.",
|
||||||
|
"es": "Un perro salvaje, desgarrado, con hambre desesperada en sus ojos. Sus huesos están visibles bajo el pelo despeinado."
|
||||||
|
},
|
||||||
"emoji": "🐕",
|
"emoji": "🐕",
|
||||||
"hp_min": 15,
|
"hp_min": 15,
|
||||||
"hp_max": 25,
|
"hp_max": 25,
|
||||||
@@ -46,8 +52,14 @@
|
|||||||
},
|
},
|
||||||
"raider_scout": {
|
"raider_scout": {
|
||||||
"npc_id": "raider_scout",
|
"npc_id": "raider_scout",
|
||||||
"name": "Raider Scout",
|
"name": {
|
||||||
"description": "A lone raider wearing makeshift armor. They eye you with hostile intent.",
|
"en": "Raider Scout",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A lone raider wearing makeshift armor. They eye you with hostile intent.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"emoji": "🏴☠️",
|
"emoji": "🏴☠️",
|
||||||
"hp_min": 30,
|
"hp_min": 30,
|
||||||
"hp_max": 45,
|
"hp_max": 45,
|
||||||
@@ -102,8 +114,14 @@
|
|||||||
},
|
},
|
||||||
"mutant_rat": {
|
"mutant_rat": {
|
||||||
"npc_id": "mutant_rat",
|
"npc_id": "mutant_rat",
|
||||||
"name": "Mutant Rat",
|
"name": {
|
||||||
"description": "A grotesquely large rat, its fur patchy and eyes glowing with unnatural light.",
|
"en": "Mutant Rat",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A grotesquely large rat, its fur patchy and eyes glowing with unnatural light.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"emoji": "🐀",
|
"emoji": "🐀",
|
||||||
"hp_min": 10,
|
"hp_min": 10,
|
||||||
"hp_max": 18,
|
"hp_max": 18,
|
||||||
@@ -140,8 +158,14 @@
|
|||||||
},
|
},
|
||||||
"infected_human": {
|
"infected_human": {
|
||||||
"npc_id": "infected_human",
|
"npc_id": "infected_human",
|
||||||
"name": "Infected Human",
|
"name": {
|
||||||
"description": "Once human, now something else. Their movements are jerky and their skin shows signs of advanced infection.",
|
"en": "Infected Human",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Once human, now something else. Their movements are jerky and their skin shows signs of advanced infection.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"emoji": "🧟",
|
"emoji": "🧟",
|
||||||
"hp_min": 35,
|
"hp_min": 35,
|
||||||
"hp_max": 50,
|
"hp_max": 50,
|
||||||
@@ -184,8 +208,14 @@
|
|||||||
},
|
},
|
||||||
"scavenger": {
|
"scavenger": {
|
||||||
"npc_id": "scavenger",
|
"npc_id": "scavenger",
|
||||||
"name": "Hostile Scavenger",
|
"name": {
|
||||||
"description": "Another survivor, but this one sees you as competition. They won't share territory.",
|
"en": "Hostile Scavenger",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Another survivor, but this one sees you as competition. They won't share territory.",
|
||||||
|
"es": ""
|
||||||
|
},
|
||||||
"emoji": "💀",
|
"emoji": "💀",
|
||||||
"hp_min": 25,
|
"hp_min": 25,
|
||||||
"hp_max": 40,
|
"hp_max": 40,
|
||||||
@@ -264,23 +294,23 @@
|
|||||||
},
|
},
|
||||||
"residential": {
|
"residential": {
|
||||||
"danger_level": 1,
|
"danger_level": 1,
|
||||||
"encounter_rate": 0.10,
|
"encounter_rate": 0.1,
|
||||||
"wandering_chance": 0.20
|
"wandering_chance": 0.2
|
||||||
},
|
},
|
||||||
"park": {
|
"park": {
|
||||||
"danger_level": 1,
|
"danger_level": 1,
|
||||||
"encounter_rate": 0.10,
|
"encounter_rate": 0.1,
|
||||||
"wandering_chance": 0.20
|
"wandering_chance": 0.2
|
||||||
},
|
},
|
||||||
"clinic": {
|
"clinic": {
|
||||||
"danger_level": 2,
|
"danger_level": 2,
|
||||||
"encounter_rate": 0.20,
|
"encounter_rate": 0.2,
|
||||||
"wandering_chance": 0.35
|
"wandering_chance": 0.35
|
||||||
},
|
},
|
||||||
"plaza": {
|
"plaza": {
|
||||||
"danger_level": 2,
|
"danger_level": 2,
|
||||||
"encounter_rate": 0.15,
|
"encounter_rate": 0.15,
|
||||||
"wandering_chance": 0.30
|
"wandering_chance": 0.3
|
||||||
},
|
},
|
||||||
"warehouse": {
|
"warehouse": {
|
||||||
"danger_level": 2,
|
"danger_level": 2,
|
||||||
@@ -290,27 +320,27 @@
|
|||||||
"warehouse_interior": {
|
"warehouse_interior": {
|
||||||
"danger_level": 2,
|
"danger_level": 2,
|
||||||
"encounter_rate": 0.22,
|
"encounter_rate": 0.22,
|
||||||
"wandering_chance": 0.40
|
"wandering_chance": 0.4
|
||||||
},
|
},
|
||||||
"overpass": {
|
"overpass": {
|
||||||
"danger_level": 3,
|
"danger_level": 3,
|
||||||
"encounter_rate": 0.30,
|
"encounter_rate": 0.3,
|
||||||
"wandering_chance": 0.45
|
"wandering_chance": 0.45
|
||||||
},
|
},
|
||||||
"office_building": {
|
"office_building": {
|
||||||
"danger_level": 3,
|
"danger_level": 3,
|
||||||
"encounter_rate": 0.25,
|
"encounter_rate": 0.25,
|
||||||
"wandering_chance": 0.40
|
"wandering_chance": 0.4
|
||||||
},
|
},
|
||||||
"office_interior": {
|
"office_interior": {
|
||||||
"danger_level": 3,
|
"danger_level": 3,
|
||||||
"encounter_rate": 0.35,
|
"encounter_rate": 0.35,
|
||||||
"wandering_chance": 0.50
|
"wandering_chance": 0.5
|
||||||
},
|
},
|
||||||
"subway": {
|
"subway": {
|
||||||
"danger_level": 4,
|
"danger_level": 4,
|
||||||
"encounter_rate": 0.35,
|
"encounter_rate": 0.35,
|
||||||
"wandering_chance": 0.50
|
"wandering_chance": 0.5
|
||||||
},
|
},
|
||||||
"subway_tunnels": {
|
"subway_tunnels": {
|
||||||
"danger_level": 4,
|
"danger_level": 4,
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
extends Control
|
|
||||||
|
|
||||||
@onready var token_input = $VBoxContainer/HBoxContainer/TokenInput
|
|
||||||
@onready var status_label = $VBoxContainer/ConnectionStatusLabel
|
|
||||||
@onready var location_name_label = $VBoxContainer/LocationNameLabel
|
|
||||||
@onready var location_image = $VBoxContainer/LocationImage
|
|
||||||
@onready var location_desc_label = $VBoxContainer/LocationDescriptionLabel
|
|
||||||
@onready var log_label = $VBoxContainer/LogLabel
|
|
||||||
|
|
||||||
var socket = WebSocketPeer.new()
|
|
||||||
var http_request : HTTPRequest
|
|
||||||
var is_connected_to_host = false
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
log_message("Godot PoC Started")
|
|
||||||
http_request = HTTPRequest.new()
|
|
||||||
add_child(http_request)
|
|
||||||
http_request.request_completed.connect(_on_image_request_completed)
|
|
||||||
|
|
||||||
func _process(delta):
|
|
||||||
socket.poll()
|
|
||||||
var state = socket.get_ready_state()
|
|
||||||
|
|
||||||
if state == WebSocketPeer.STATE_OPEN:
|
|
||||||
if not is_connected_to_host:
|
|
||||||
is_connected_to_host = true
|
|
||||||
status_label.text = "Status: Connected"
|
|
||||||
log_message("WebSocket Connected!")
|
|
||||||
|
|
||||||
while socket.get_available_packet_count():
|
|
||||||
var packet = socket.get_packet()
|
|
||||||
var data = packet.get_string_from_utf8()
|
|
||||||
var json = JSON.new()
|
|
||||||
var error = json.parse(data)
|
|
||||||
if error == OK:
|
|
||||||
handle_message(json.get_data())
|
|
||||||
else:
|
|
||||||
log_message("Error parsing JSON: " + data)
|
|
||||||
|
|
||||||
elif state == WebSocketPeer.STATE_CLOSED:
|
|
||||||
if is_connected_to_host:
|
|
||||||
is_connected_to_host = false
|
|
||||||
status_label.text = "Status: Disconnected"
|
|
||||||
log_message("WebSocket Disconnected")
|
|
||||||
|
|
||||||
func _on_connect_button_pressed():
|
|
||||||
var token = token_input.text.strip_edges()
|
|
||||||
if token == "":
|
|
||||||
log_message("Please enter a token.")
|
|
||||||
return
|
|
||||||
|
|
||||||
var url = "wss://api-staging.echoesoftheash.com/ws/game/" + token
|
|
||||||
log_message("Connecting to: " + url)
|
|
||||||
var err = socket.connect_to_url(url)
|
|
||||||
if err != OK:
|
|
||||||
log_message("Error connecting to URL: " + str(err))
|
|
||||||
else:
|
|
||||||
status_label.text = "Status: Connecting..."
|
|
||||||
|
|
||||||
func handle_message(msg):
|
|
||||||
# log_message("Received: " + str(msg.get("type")))
|
|
||||||
|
|
||||||
if msg.get("type") == "location_update":
|
|
||||||
var data = msg.get("data", {})
|
|
||||||
var location = data.get("location", {})
|
|
||||||
|
|
||||||
if location:
|
|
||||||
update_location_ui(location)
|
|
||||||
|
|
||||||
func update_location_ui(location):
|
|
||||||
location_name_label.text = location.get("name", "Unknown Location")
|
|
||||||
location_desc_label.text = location.get("description", "")
|
|
||||||
|
|
||||||
var image_url = location.get("image_url", "")
|
|
||||||
if image_url != "":
|
|
||||||
fetch_image(image_url)
|
|
||||||
|
|
||||||
func fetch_image(url):
|
|
||||||
if url.begins_with("/"):
|
|
||||||
url = "https://api-staging.echoesoftheash.com" + url
|
|
||||||
|
|
||||||
log_message("Fetching image: " + url)
|
|
||||||
http_request.cancel_request()
|
|
||||||
http_request.request(url)
|
|
||||||
|
|
||||||
func _on_image_request_completed(result, response_code, headers, body):
|
|
||||||
if result == HTTPRequest.RESULT_SUCCESS:
|
|
||||||
var image = Image.new()
|
|
||||||
var error = image.load_png_from_buffer(body)
|
|
||||||
if error != OK:
|
|
||||||
error = image.load_jpg_from_buffer(body)
|
|
||||||
if error != OK:
|
|
||||||
error = image.load_webp_from_buffer(body)
|
|
||||||
|
|
||||||
if error == OK:
|
|
||||||
var texture = ImageTexture.create_from_image(image)
|
|
||||||
location_image.texture = texture
|
|
||||||
else:
|
|
||||||
log_message("Failed to load image texture")
|
|
||||||
else:
|
|
||||||
log_message("Failed to fetch image. Code: " + str(response_code))
|
|
||||||
|
|
||||||
func log_message(text):
|
|
||||||
print(text)
|
|
||||||
log_label.text += text + "\n"
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3 uid="uid://c8q7y6x5z4w3"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://Main.gd" id="1_m4i3n"]
|
|
||||||
|
|
||||||
[node name="Main" type="Control"]
|
|
||||||
layout_mode = 3
|
|
||||||
anchors_preset = 15
|
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
script = ExtResource("1_m4i3n")
|
|
||||||
|
|
||||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
|
||||||
layout_mode = 1
|
|
||||||
anchors_preset = 15
|
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
|
|
||||||
[node name="HeaderLabel" type="Label" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
text = "Echoes of the Ashes - Godot PoC"
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="TokenInput" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
size_flags_horizontal = 3
|
|
||||||
placeholder_text = "Enter Auth Token"
|
|
||||||
|
|
||||||
[node name="ConnectButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
text = "Connect via WebSocket"
|
|
||||||
|
|
||||||
[node name="ConnectionStatusLabel" type="Label" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
text = "Status: Disconnected"
|
|
||||||
|
|
||||||
[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="LocationNameLabel" type="Label" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="LocationImage" type="TextureRect" parent="VBoxContainer"]
|
|
||||||
custom_minimum_size = Vector2(0, 300)
|
|
||||||
layout_mode = 2
|
|
||||||
expand_mode = 1
|
|
||||||
stretch_mode = 5
|
|
||||||
|
|
||||||
[node name="LocationDescriptionLabel" type="RichTextLabel" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
fit_content = true
|
|
||||||
|
|
||||||
[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="LogLabel" type="RichTextLabel" parent="VBoxContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
size_flags_vertical = 3
|
|
||||||
text = "Logs will appear here...
|
|
||||||
"
|
|
||||||
|
|
||||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ConnectButton" to="." method="_on_connect_button_pressed"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect fill="#364966" height="128" width="128" rx="20" ry="20"/><path d="M64 16 L16 112 L112 112 Z" fill="#ffffff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 187 B |
@@ -1,13 +0,0 @@
|
|||||||
config_version=5
|
|
||||||
|
|
||||||
[application]
|
|
||||||
|
|
||||||
config/name="Echoes of the Ashes PoC"
|
|
||||||
config/features=PackedStringArray("4.5", "Forward Plus")
|
|
||||||
run/main_scene="res://Main.tscn"
|
|
||||||
config/icon="res://icon.svg"
|
|
||||||
|
|
||||||
[display]
|
|
||||||
|
|
||||||
window/size/viewport_width=1280
|
|
||||||
window/size/viewport_height=720
|
|
||||||
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 678 KiB After Width: | Height: | Size: 678 KiB |
|
Before Width: | Height: | Size: 881 KiB After Width: | Height: | Size: 881 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 602 KiB |
|
Before Width: | Height: | Size: 367 KiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 528 KiB |
|
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 597 KiB |
|
Before Width: | Height: | Size: 859 KiB After Width: | Height: | Size: 859 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 597 KiB |
|
Before Width: | Height: | Size: 758 KiB After Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 552 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 677 KiB After Width: | Height: | Size: 677 KiB |
|
Before Width: | Height: | Size: 804 KiB After Width: | Height: | Size: 804 KiB |
|
Before Width: | Height: | Size: 507 KiB After Width: | Height: | Size: 507 KiB |
|
Before Width: | Height: | Size: 538 KiB After Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 947 KiB After Width: | Height: | Size: 947 KiB |
|
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 698 KiB |
|
Before Width: | Height: | Size: 822 KiB After Width: | Height: | Size: 822 KiB |
|
Before Width: | Height: | Size: 535 KiB After Width: | Height: | Size: 535 KiB |
|
Before Width: | Height: | Size: 961 KiB After Width: | Height: | Size: 961 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 905 KiB After Width: | Height: | Size: 905 KiB |
|
Before Width: | Height: | Size: 980 KiB After Width: | Height: | Size: 980 KiB |
|
Before Width: | Height: | Size: 803 KiB After Width: | Height: | Size: 803 KiB |
|
Before Width: | Height: | Size: 864 KiB After Width: | Height: | Size: 864 KiB |
|
Before Width: | Height: | Size: 694 KiB After Width: | Height: | Size: 694 KiB |
|
Before Width: | Height: | Size: 777 KiB After Width: | Height: | Size: 777 KiB |
|
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
|
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 636 KiB |
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 860 KiB After Width: | Height: | Size: 860 KiB |
|
Before Width: | Height: | Size: 648 KiB After Width: | Height: | Size: 648 KiB |
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 898 KiB After Width: | Height: | Size: 898 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 933 KiB After Width: | Height: | Size: 933 KiB |
|
Before Width: | Height: | Size: 460 KiB After Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 749 KiB After Width: | Height: | Size: 749 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
57
images-source/make_webp.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Script to convert original images to optimized WebP format
|
||||||
|
# Run this script from the images-source directory
|
||||||
|
# Source files: ./ (current directory)
|
||||||
|
# Output files: ../images/
|
||||||
|
|
||||||
|
SOURCE_DIR="."
|
||||||
|
OUTPUT_DIR="../images"
|
||||||
|
ITEM_SIZE="256x256"
|
||||||
|
|
||||||
|
echo "🔄 Starting image conversion..."
|
||||||
|
echo " Source: $SOURCE_DIR"
|
||||||
|
echo " Output: $OUTPUT_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for category in items locations npcs interactables; do
|
||||||
|
src="$SOURCE_DIR/$category"
|
||||||
|
out="$OUTPUT_DIR/$category"
|
||||||
|
|
||||||
|
if [[ ! -d "$src" ]]; then
|
||||||
|
echo "⚠️ Skipping $category (source not found)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$out"
|
||||||
|
echo "📂 Processing $category..."
|
||||||
|
|
||||||
|
find "$src" -maxdepth 1 -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" \) | while read -r img; do
|
||||||
|
filename="${img##*/}"
|
||||||
|
base="${filename%.*}"
|
||||||
|
out_file="$out/$base.webp"
|
||||||
|
|
||||||
|
if [[ -f "$out_file" ]]; then
|
||||||
|
echo " ✔ Exists: $base.webp"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$category" == "items" ]]; then
|
||||||
|
# Special processing for items: remove white background and resize
|
||||||
|
echo " ➜ Converting item: $filename"
|
||||||
|
tmp="/tmp/${base}_clean.png"
|
||||||
|
convert "$img" -fuzz 10% -transparent white -resize "$ITEM_SIZE" "$tmp"
|
||||||
|
cwebp "$tmp" -q 85 -o "$out_file" >/dev/null
|
||||||
|
rm "$tmp"
|
||||||
|
else
|
||||||
|
# Standard conversion for other categories
|
||||||
|
echo " ➜ Converting: $filename"
|
||||||
|
cwebp "$img" -q 85 -o "$out_file" >/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✨ Done! WebP files generated in $OUTPUT_DIR"
|
||||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Set size for item icons (example: 256x256 or 128x128)
|
|
||||||
ITEM_SIZE="256x256"
|
|
||||||
|
|
||||||
echo "Starting conversion..."
|
|
||||||
|
|
||||||
find . -type d -name "original" | while read -r orig_dir; do
|
|
||||||
echo "📂 Folder: $orig_dir"
|
|
||||||
|
|
||||||
out_dir="$(dirname "$orig_dir")"
|
|
||||||
|
|
||||||
# Check if this is the items/original folder
|
|
||||||
is_items_folder=false
|
|
||||||
if [[ "$orig_dir" == *"/items/original" ]]; then
|
|
||||||
is_items_folder=true
|
|
||||||
echo " 🔧 Applying item-specific processing (remove white background, resize)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Process images in original folder
|
|
||||||
find "$orig_dir" -maxdepth 1 -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" \) | \
|
|
||||||
while read -r img; do
|
|
||||||
|
|
||||||
filename="$(basename "$img")"
|
|
||||||
base="${filename%.*}"
|
|
||||||
out_file="$out_dir/$base.webp"
|
|
||||||
|
|
||||||
if [[ -f "$out_file" ]]; then
|
|
||||||
echo " ✔ Skipping existing: $out_file"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If this is an item icon, preprocess it
|
|
||||||
if [[ "$is_items_folder" = true ]]; then
|
|
||||||
echo " ➜ Processing item: $filename"
|
|
||||||
|
|
||||||
tmp_png="/tmp/${base}_clean.png"
|
|
||||||
|
|
||||||
# 1. Remove white background using ImageMagick
|
|
||||||
convert "$img" -fuzz 10% -transparent white "$tmp_png"
|
|
||||||
|
|
||||||
# 2. Resize to smaller square
|
|
||||||
convert "$tmp_png" -resize "$ITEM_SIZE" "$tmp_png"
|
|
||||||
|
|
||||||
# 3. Convert to WebP
|
|
||||||
cwebp "$tmp_png" -q 85 -o "$out_file" >/dev/null
|
|
||||||
|
|
||||||
rm "$tmp_png"
|
|
||||||
|
|
||||||
else
|
|
||||||
# Standard conversion for other folders
|
|
||||||
echo " ➜ Converting: $filename → $out_file"
|
|
||||||
cwebp "$img" -q 85 -o "$out_file" >/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "✨ Done! Missing files created, with special processing for items."
|
|
||||||
188
new-readme.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Echoes of the Ash
|
||||||
|
|
||||||
|
> A post-apocalyptic survival RPG - Browser-based MUD-style game
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 🎮 What is Echoes of the Ash?
|
||||||
|
|
||||||
|
Echoes of the Ash is a **browser-based RPG** set in a dark, post-apocalyptic world. Inspired by classic MUD (Multi-User Dungeon) games, it combines text-driven exploration with visual elements, real-time combat, and survival mechanics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Current Game Features
|
||||||
|
|
||||||
|
### Core Systems
|
||||||
|
|
||||||
|
| Feature | Status | Description |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| **Character System** | ✅ Complete | Create characters with 4 stats (Strength, Agility, Endurance, Intellect) |
|
||||||
|
| **Health & Stamina** | ✅ Complete | HP/Stamina management with visual progress bars |
|
||||||
|
| **Leveling & XP** | ✅ Complete | XP-based progression with stat point allocation |
|
||||||
|
| **Inventory** | ✅ Complete | Weight/volume-based carrying capacity |
|
||||||
|
| **Equipment** | ✅ Complete | Weapon, armor, and backpack slots |
|
||||||
|
| **Combat (PvE)** | ✅ Complete | Turn-based combat with visual effects |
|
||||||
|
| **Combat (PvP)** | ✅ Complete | Player vs Player combat system |
|
||||||
|
| **Real-time Updates** | ✅ Complete | WebSocket-based live game state |
|
||||||
|
|
||||||
|
### Exploration & Interaction
|
||||||
|
|
||||||
|
| Feature | Status | Description |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| **World Map** | ✅ Complete | Graph-based location system with connections |
|
||||||
|
| **Movement** | ✅ Complete | Navigate between connected locations |
|
||||||
|
| **Interactables** | ✅ Complete | Search containers, objects for loot |
|
||||||
|
| **Enemy Spawning** | ✅ Complete | Static and wandering NPCs |
|
||||||
|
| **Corpse Looting** | ✅ Complete | Loot fallen enemies and players |
|
||||||
|
| **Dropped Items** | ✅ Complete | Pick up items on the ground |
|
||||||
|
|
||||||
|
### Crafting & Economy
|
||||||
|
|
||||||
|
| Feature | Status | Description |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| **Workbench** | ✅ Complete | Craft, repair, and salvage items |
|
||||||
|
| **Crafting System** | ✅ Complete | Create items from materials |
|
||||||
|
| **Repair System** | ✅ Complete | Restore durability to equipment |
|
||||||
|
| **Salvage System** | ✅ Complete | Break down items for materials |
|
||||||
|
|
||||||
|
### Social & Multiplayer
|
||||||
|
|
||||||
|
| Feature | Status | Description |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| **Accounts** | ✅ Complete | Registration, login, JWT authentication |
|
||||||
|
| **Multiple Characters** | ✅ Complete | Create up to 3 characters per account |
|
||||||
|
| **Leaderboards** | ✅ Complete | Rankings by level, kills, XP |
|
||||||
|
| **Player Profiles** | ✅ Complete | View player stats and equipment |
|
||||||
|
| **Online Players** | ✅ Complete | See who's currently online |
|
||||||
|
|
||||||
|
### Platforms
|
||||||
|
|
||||||
|
| Platform | Status | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| **Web Browser** | ✅ Complete | Play at any time via modern browser |
|
||||||
|
| **PWA (Mobile)** | ✅ Complete | Install as app on mobile devices |
|
||||||
|
| **Electron Desktop** | ✅ Complete | Standalone Windows/Mac/Linux app |
|
||||||
|
| **Steam Integration** | 🔧 Setup | Steamworks SDK ready for deployment |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 What Can Players Do?
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
1. **Create an Account** - Register with username and password
|
||||||
|
2. **Create a Character** - Name your survivor and choose starting stats
|
||||||
|
3. **Enter the World** - Spawn at the starting location
|
||||||
|
|
||||||
|
### Gameplay Loop
|
||||||
|
1. **Explore** - Move between connected locations to discover new areas
|
||||||
|
2. **Scavenge** - Search containers, corpses, and interactables for supplies
|
||||||
|
3. **Fight** - Engage hostile NPCs in turn-based combat
|
||||||
|
4. **Craft** - Use workbenches to create, repair, or salvage items
|
||||||
|
5. **Level Up** - Gain XP from combat and allocate stat points
|
||||||
|
6. **Survive** - Manage HP, stamina, and inventory weight
|
||||||
|
|
||||||
|
### Combat
|
||||||
|
- **Attack** enemies with equipped weapons
|
||||||
|
- **Use Items** during battle (healing, buffs)
|
||||||
|
- **Flee** when outmatched (success based on Agility)
|
||||||
|
- **PvP** - Challenge other players in combat
|
||||||
|
|
||||||
|
### Character Progression
|
||||||
|
- **4 Core Stats**: Strength, Agility, Endurance, Intellect
|
||||||
|
- **Equipment**: Weapons, armor, backpacks
|
||||||
|
- **Stat Points**: Earn 1 per level to customize your build
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Technical Stack
|
||||||
|
|
||||||
|
### Frontend (PWA)
|
||||||
|
- **Framework**: React 18 + TypeScript
|
||||||
|
- **Build Tool**: Vite
|
||||||
|
- **State Management**: Zustand
|
||||||
|
- **Real-time**: WebSocket connections
|
||||||
|
- **Styling**: Custom CSS with dark theme
|
||||||
|
|
||||||
|
### Backend (API)
|
||||||
|
- **Framework**: FastAPI (Python)
|
||||||
|
- **Database**: SQLite (development) / PostgreSQL (production)
|
||||||
|
- **Cache**: Redis for real-time state
|
||||||
|
- **Auth**: JWT tokens
|
||||||
|
|
||||||
|
### Desktop (Electron)
|
||||||
|
- **Framework**: Electron 28
|
||||||
|
- **Steam SDK**: steamworks.js integration
|
||||||
|
- **Builds**: Windows (NSIS, Portable), Linux (AppImage, DEB), macOS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Asset Summary
|
||||||
|
|
||||||
|
| Category | Count | Size |
|
||||||
|
|----------|-------|------|
|
||||||
|
| Location Images | 14 | - |
|
||||||
|
| Item Images | 40 | - |
|
||||||
|
| NPC Images | 5 | - |
|
||||||
|
| Interactable Images | 8 | - |
|
||||||
|
| Icon Sets | 1 | - |
|
||||||
|
| **Total Images** | **134 files** | **~79 MB** |
|
||||||
|
| Sound Effects | 0 | 0 |
|
||||||
|
| Music | 0 | 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- [ ] Sound effects and ambient music
|
||||||
|
- [ ] Quest/mission system
|
||||||
|
- [ ] NPC dialogue trees
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
- [ ] Crafting recipes expansion
|
||||||
|
- [ ] Faction/reputation system
|
||||||
|
- [ ] Player trading
|
||||||
|
- [ ] Housing/storage
|
||||||
|
- [ ] Skill tree system
|
||||||
|
- [ ] Status effects (poison, bleeding, etc.)
|
||||||
|
- [ ] Weather/day-night cycle
|
||||||
|
- [ ] Achievements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Running the Game
|
||||||
|
|
||||||
|
### Web/PWA (Docker)
|
||||||
|
```bash
|
||||||
|
docker compose up echoes_of_the_ashes_pwa echoes_of_the_ashes_api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Electron Development
|
||||||
|
```bash
|
||||||
|
cd pwa
|
||||||
|
npm install
|
||||||
|
npm run electron:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Electron Apps
|
||||||
|
```bash
|
||||||
|
npm run electron:build:win # Windows
|
||||||
|
npm run electron:build:linux # Linux
|
||||||
|
npm run electron:build:mac # macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Additional Documentation
|
||||||
|
|
||||||
|
- [Game Mechanics](docs/game/MECHANICS.md) - Detailed gameplay systems
|
||||||
|
- [API Documentation](docs/api/) - Backend endpoints reference
|
||||||
|
- [Development Guide](docs/development/) - Contributing and architecture
|
||||||
|
- [Map Editor](web-map/README.md) - World building tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 1.0.0-alpha
|
||||||
|
**Last Updated**: December 2025
|
||||||
4
pwa/.gitignore
vendored
@@ -6,6 +6,10 @@ yarn.lock
|
|||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
dist-electron/
|
||||||
|
|
||||||
|
# Copied assets (generated at build time)
|
||||||
|
public/images/
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
|||||||
29
pwa/electron/afterPack.cjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* electron-builder afterPack hook
|
||||||
|
* Removes "type": "module" from the packaged package.json
|
||||||
|
* to ensure CommonJS files work correctly in the Electron app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
exports.default = async function (context) {
|
||||||
|
const appDir = context.appOutDir
|
||||||
|
const resourcesPath = path.join(appDir, 'resources', 'app')
|
||||||
|
const packageJsonPath = path.join(resourcesPath, 'package.json')
|
||||||
|
|
||||||
|
console.log('afterPack: Checking for package.json at:', packageJsonPath)
|
||||||
|
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||||
|
|
||||||
|
// Remove the "type": "module" field to ensure CommonJS compat
|
||||||
|
if (packageJson.type === 'module') {
|
||||||
|
delete packageJson.type
|
||||||
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
||||||
|
console.log('afterPack: Removed "type": "module" from package.json')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('afterPack: package.json not found, skipping...')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ function createWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: path.join(__dirname, 'preload.js')
|
preload: path.join(__dirname, 'preload.cjs')
|
||||||
},
|
},
|
||||||
icon: path.join(__dirname, 'icons/icon.png'),
|
icon: path.join(__dirname, 'icons/icon.png'),
|
||||||
title: 'Echoes of the Ash'
|
title: 'Echoes of the Ash'
|
||||||
@@ -9,17 +9,18 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://echoesoftheash.com",
|
"homepage": "https://echoesoftheash.com",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"copy-assets": "rm -rf ./public/images && cp -r ../images ./public/",
|
||||||
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
|
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
|
||||||
"electron:build": "npm run build && electron-builder",
|
"electron:build": "npm run copy-assets && npm run build && electron-builder",
|
||||||
"electron:build:win": "npm run build && electron-builder --win",
|
"electron:build:win": "npm run copy-assets && npm run build && electron-builder --win",
|
||||||
"electron:build:linux": "npm run build && electron-builder --linux",
|
"electron:build:linux": "npm run copy-assets && npm run build && electron-builder --linux",
|
||||||
"electron:build:mac": "npm run build && electron-builder --mac"
|
"electron:build:mac": "npm run copy-assets && npm run build && electron-builder --mac"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -27,7 +28,10 @@
|
|||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"zustand": "^4.4.7",
|
"zustand": "^4.4.7",
|
||||||
"twemoji": "^14.0.2"
|
"twemoji": "^14.0.2",
|
||||||
|
"i18next": "^23.7.0",
|
||||||
|
"react-i18next": "^14.0.0",
|
||||||
|
"i18next-browser-languagedetector": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
@@ -52,6 +56,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "com.echoesoftheash.game",
|
"appId": "com.echoesoftheash.game",
|
||||||
"productName": "Echoes of the Ash",
|
"productName": "Echoes of the Ash",
|
||||||
|
"afterPack": "./electron/afterPack.cjs",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist-electron"
|
"output": "dist-electron"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useNavigate, useLocation } from 'react-router-dom'
|
|||||||
import { useAuth } from '../hooks/useAuth'
|
import { useAuth } from '../hooks/useAuth'
|
||||||
import { useGameWebSocket } from '../hooks/useGameWebSocket'
|
import { useGameWebSocket } from '../hooks/useGameWebSocket'
|
||||||
import api from '../services/api'
|
import api from '../services/api'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import LanguageSelector from './LanguageSelector'
|
||||||
import './Game.css'
|
import './Game.css'
|
||||||
|
|
||||||
interface GameHeaderProps {
|
interface GameHeaderProps {
|
||||||
@@ -13,6 +15,7 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { currentCharacter, logout } = useAuth()
|
const { currentCharacter, logout } = useAuth()
|
||||||
|
const { t } = useTranslation()
|
||||||
const [playerCount, setPlayerCount] = useState<number>(0)
|
const [playerCount, setPlayerCount] = useState<number>(0)
|
||||||
|
|
||||||
// Fetch initial player count
|
// Fetch initial player count
|
||||||
@@ -63,19 +66,20 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
|
|||||||
onClick={() => navigate('/game')}
|
onClick={() => navigate('/game')}
|
||||||
className={`nav-link ${isActive('/game') ? 'active' : ''}`}
|
className={`nav-link ${isActive('/game') ? 'active' : ''}`}
|
||||||
>
|
>
|
||||||
🎮 Game
|
🎮 {t('common.game')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/leaderboards')}
|
onClick={() => navigate('/leaderboards')}
|
||||||
className={`nav-link ${isActive('/leaderboards') ? 'active' : ''}`}
|
className={`nav-link ${isActive('/leaderboards') ? 'active' : ''}`}
|
||||||
>
|
>
|
||||||
🏆 Leaderboards
|
🏆 {t('common.leaderboards')}
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="user-info">
|
<div className="user-info">
|
||||||
<div className="player-count-badge" title="Online Players">
|
<LanguageSelector />
|
||||||
|
<div className="player-count-badge" title={t('game.onlineCount', { count: playerCount })}>
|
||||||
<span className="status-dot"></span>
|
<span className="status-dot"></span>
|
||||||
<span className="count-text">{playerCount} Online</span>
|
<span className="count-text">{t('game.onlineCount', { count: playerCount })}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
|
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
|
||||||
@@ -87,9 +91,9 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
|
|||||||
onClick={() => navigate('/account')}
|
onClick={() => navigate('/account')}
|
||||||
className="button-secondary"
|
className="button-secondary"
|
||||||
>
|
>
|
||||||
Account
|
{t('common.account')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={logout} className="button-secondary">Logout</button>
|
<button onClick={logout} className="button-secondary">{t('auth.logout')}</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
86
pwa/src/components/LanguageSelector.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
.language-selector {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-flag {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-code {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector:hover .language-dropdown,
|
||||||
|
.language-selector:focus-within .language-dropdown {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option.active {
|
||||||
|
background: rgba(100, 100, 255, 0.2);
|
||||||
|
color: #8bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-name {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
40
pwa/src/components/LanguageSelector.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import './LanguageSelector.css'
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
||||||
|
{ code: 'es', name: 'Español', flag: '🇪🇸' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function LanguageSelector() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
const changeLanguage = (langCode: string) => {
|
||||||
|
i18n.changeLanguage(langCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLang = languages.find(l => l.code === i18n.language) || languages[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="language-selector">
|
||||||
|
<button className="language-btn">
|
||||||
|
<span className="lang-flag">{currentLang.flag}</span>
|
||||||
|
<span className="lang-code">{currentLang.code.toUpperCase()}</span>
|
||||||
|
</button>
|
||||||
|
<div className="language-dropdown">
|
||||||
|
{languages.map(lang => (
|
||||||
|
<button
|
||||||
|
key={lang.code}
|
||||||
|
className={`lang-option ${i18n.language === lang.code ? 'active' : ''}`}
|
||||||
|
onClick={() => changeLanguage(lang.code)}
|
||||||
|
>
|
||||||
|
<span className="lang-flag">{lang.flag}</span>
|
||||||
|
<span className="lang-name">{lang.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguageSelector
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { MouseEvent, ChangeEvent } from 'react'
|
import { MouseEvent, ChangeEvent } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { PlayerState, Profile, Equipment } from './types'
|
import { PlayerState, Profile, Equipment } from './types'
|
||||||
|
import { getAssetPath } from '../../utils/assetPath'
|
||||||
|
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||||
import './InventoryModal.css'
|
import './InventoryModal.css'
|
||||||
|
|
||||||
interface InventoryModalProps {
|
interface InventoryModalProps {
|
||||||
@@ -31,6 +34,7 @@ function InventoryModal({
|
|||||||
onUnequipItem,
|
onUnequipItem,
|
||||||
onDropItem
|
onDropItem
|
||||||
}: InventoryModalProps) {
|
}: InventoryModalProps) {
|
||||||
|
useTranslation()
|
||||||
// Categories for the sidebar
|
// Categories for the sidebar
|
||||||
const categories = [
|
const categories = [
|
||||||
{ id: 'all', label: 'All Items', icon: '🎒' },
|
{ id: 'all', label: 'All Items', icon: '🎒' },
|
||||||
@@ -51,7 +55,7 @@ function InventoryModal({
|
|||||||
// Filter items based on search and category
|
// Filter items based on search and category
|
||||||
const filteredItems = allItems
|
const filteredItems = allItems
|
||||||
.filter((item: any) => {
|
.filter((item: any) => {
|
||||||
const itemName = item.name || 'Unknown Item';
|
const itemName = getTranslatedText(item.name) || 'Unknown Item';
|
||||||
const matchesSearch = itemName.toLowerCase().includes(inventoryFilter.toLowerCase())
|
const matchesSearch = itemName.toLowerCase().includes(inventoryFilter.toLowerCase())
|
||||||
const matchesCategory = inventoryCategoryFilter === 'all' || item.type === inventoryCategoryFilter
|
const matchesCategory = inventoryCategoryFilter === 'all' || item.type === inventoryCategoryFilter
|
||||||
return matchesSearch && matchesCategory
|
return matchesSearch && matchesCategory
|
||||||
@@ -60,7 +64,7 @@ function InventoryModal({
|
|||||||
// Equipped items first
|
// Equipped items first
|
||||||
if (a.is_equipped && !b.is_equipped) return -1;
|
if (a.is_equipped && !b.is_equipped) return -1;
|
||||||
if (!a.is_equipped && b.is_equipped) return 1;
|
if (!a.is_equipped && b.is_equipped) return 1;
|
||||||
return (a.name || '').localeCompare(b.name || '');
|
return (getTranslatedText(a.name) || '').localeCompare(getTranslatedText(b.name) || '');
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderItemCard = (item: any, i: number) => {
|
const renderItemCard = (item: any, i: number) => {
|
||||||
@@ -75,8 +79,8 @@ function InventoryModal({
|
|||||||
<div className="item-image-section small">
|
<div className="item-image-section small">
|
||||||
{item.image_path ? (
|
{item.image_path ? (
|
||||||
<img
|
<img
|
||||||
src={item.image_path}
|
src={getAssetPath(item.image_path)}
|
||||||
alt={item.name}
|
alt={getTranslatedText(item.name)}
|
||||||
className="item-img-thumb"
|
className="item-img-thumb"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
@@ -95,7 +99,7 @@ function InventoryModal({
|
|||||||
<div className="item-info-section">
|
<div className="item-info-section">
|
||||||
<div className="item-header-compact">
|
<div className="item-header-compact">
|
||||||
<span className="item-emoji-inline">{item.emoji}</span>
|
<span className="item-emoji-inline">{item.emoji}</span>
|
||||||
<h4 className={`item-name-compact text-tier-${item.tier || 0}`}>{item.name}</h4>
|
<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">Equipped</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,7 +120,7 @@ function InventoryModal({
|
|||||||
|
|
||||||
{/* Stats & Durability */}
|
{/* Stats & Durability */}
|
||||||
<div className="stats-durability-column">
|
<div className="stats-durability-column">
|
||||||
{item.description && <p className="item-description-compact">{item.description}</p>}
|
{item.description && <p className="item-description-compact">{getTranslatedText(item.description)}</p>}
|
||||||
|
|
||||||
{/* Stats Row - Button-like Badges */}
|
{/* Stats Row - Button-like Badges */}
|
||||||
<div className="stat-badges-container">
|
<div className="stat-badges-container">
|
||||||
@@ -315,7 +319,7 @@ function InventoryModal({
|
|||||||
{equipment?.backpack ? (
|
{equipment?.backpack ? (
|
||||||
<div className="backpack-status active">
|
<div className="backpack-status active">
|
||||||
<span className="backpack-icon">🎒</span>
|
<span className="backpack-icon">🎒</span>
|
||||||
<span className="backpack-name">{equipment.backpack.name}</span>
|
<span className="backpack-name">{getTranslatedText(equipment.backpack.name)}</span>
|
||||||
<span className="backpack-stats">
|
<span className="backpack-stats">
|
||||||
(+{equipment.backpack.unique_stats?.weight_capacity || equipment.backpack.stats?.weight_capacity || 0}kg /
|
(+{equipment.backpack.unique_stats?.weight_capacity || equipment.backpack.stats?.weight_capacity || 0}kg /
|
||||||
+{equipment.backpack.unique_stats?.volume_capacity || equipment.backpack.stats?.volume_capacity || 0}L)
|
+{equipment.backpack.unique_stats?.volume_capacity || equipment.backpack.stats?.volume_capacity || 0}L)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
|
||||||
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
|
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Workbench from './Workbench'
|
import Workbench from './Workbench'
|
||||||
|
import { getAssetPath } from '../../utils/assetPath'
|
||||||
|
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||||
|
|
||||||
interface LocationViewProps {
|
interface LocationViewProps {
|
||||||
location: Location
|
location: Location
|
||||||
@@ -79,11 +82,12 @@ function LocationView({
|
|||||||
onRepair,
|
onRepair,
|
||||||
onUncraft
|
onUncraft
|
||||||
}: LocationViewProps) {
|
}: LocationViewProps) {
|
||||||
|
useTranslation()
|
||||||
return (
|
return (
|
||||||
<div className="location-view">
|
<div className="location-view">
|
||||||
<div className="location-info">
|
<div className="location-info">
|
||||||
<h2 className="centered-heading">
|
<h2 className="centered-heading">
|
||||||
{location.name}
|
{getTranslatedText(location.name)}
|
||||||
{location.danger_level !== undefined && location.danger_level === 0 && (
|
{location.danger_level !== undefined && location.danger_level === 0 && (
|
||||||
<span className="danger-badge danger-safe" title="Safe Zone">✓ Safe</span>
|
<span className="danger-badge danger-safe" title="Safe Zone">✓ Safe</span>
|
||||||
)}
|
)}
|
||||||
@@ -132,8 +136,8 @@ function LocationView({
|
|||||||
{location.image_url && (
|
{location.image_url && (
|
||||||
<div className="location-image-container">
|
<div className="location-image-container">
|
||||||
<img
|
<img
|
||||||
src={location.image_url}
|
src={getAssetPath(location.image_url)}
|
||||||
alt={location.name}
|
alt={getTranslatedText(location.name)}
|
||||||
className="location-image"
|
className="location-image"
|
||||||
onError={(e: any) => (e.currentTarget.style.display = 'none')}
|
onError={(e: any) => (e.currentTarget.style.display = 'none')}
|
||||||
/>
|
/>
|
||||||
@@ -141,7 +145,7 @@ function LocationView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="location-description-box">
|
<div className="location-description-box">
|
||||||
<p className="location-description">{location.description}</p>
|
<p className="location-description">{getTranslatedText(location.description)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,7 +180,7 @@ function LocationView({
|
|||||||
{enemy.id && (
|
{enemy.id && (
|
||||||
<div className="entity-image">
|
<div className="entity-image">
|
||||||
<img
|
<img
|
||||||
src={enemy.image_path ? `/${enemy.image_path}` : `/images/npcs/${enemy.name.toLowerCase().replace(/ /g, '_')}.webp`}
|
src={getAssetPath(enemy.image_path || `images/npcs/${enemy.name.toLowerCase().replace(/ /g, '_')}.webp`)}
|
||||||
alt={enemy.name}
|
alt={enemy.name}
|
||||||
onError={(e: any) => { e.currentTarget.style.display = 'none' }}
|
onError={(e: any) => { e.currentTarget.style.display = 'none' }}
|
||||||
/>
|
/>
|
||||||
@@ -301,7 +305,7 @@ function LocationView({
|
|||||||
<div key={i} className="entity-card item-card">
|
<div key={i} className="entity-card item-card">
|
||||||
{item.image_path ? (
|
{item.image_path ? (
|
||||||
<img
|
<img
|
||||||
src={item.image_path}
|
src={getAssetPath(item.image_path)}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
className="entity-icon"
|
className="entity-icon"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Location, Profile, CombatState } from './types'
|
import type { Location, Profile, CombatState } from './types'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { getAssetPath } from '../../utils/assetPath'
|
||||||
|
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||||
|
|
||||||
interface MovementControlsProps {
|
interface MovementControlsProps {
|
||||||
location: Location
|
location: Location
|
||||||
@@ -45,7 +47,7 @@ function MovementControls({
|
|||||||
// Helper function to get destination name for a direction
|
// Helper function to get destination name for a direction
|
||||||
const getDestinationName = (direction: string): string => {
|
const getDestinationName = (direction: string): string => {
|
||||||
const detail = getDirectionDetail(direction)
|
const detail = getDirectionDetail(direction)
|
||||||
return detail ? (detail.destination_name || detail.destination) : ''
|
return detail ? getTranslatedText(detail.destination_name || detail.destination) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get distance for a direction
|
// Helper function to get distance for a direction
|
||||||
@@ -197,8 +199,8 @@ function MovementControls({
|
|||||||
{interactable.image_path && (
|
{interactable.image_path && (
|
||||||
<div className="interactable-image-container">
|
<div className="interactable-image-container">
|
||||||
<img
|
<img
|
||||||
src={`/${interactable.image_path}`}
|
src={getAssetPath(interactable.image_path)}
|
||||||
alt={interactable.name}
|
alt={getTranslatedText(interactable.name)}
|
||||||
className="interactable-image"
|
className="interactable-image"
|
||||||
onError={(e: any) => {
|
onError={(e: any) => {
|
||||||
e.currentTarget.style.display = 'none'
|
e.currentTarget.style.display = 'none'
|
||||||
@@ -208,7 +210,7 @@ function MovementControls({
|
|||||||
)}
|
)}
|
||||||
<div className="interactable-content">
|
<div className="interactable-content">
|
||||||
<div className="interactable-header">
|
<div className="interactable-header">
|
||||||
<span className="interactable-name">{interactable.name}</span>
|
<span className="interactable-name">{getTranslatedText(interactable.name)}</span>
|
||||||
</div>
|
</div>
|
||||||
{interactable.actions && interactable.actions.length > 0 && (
|
{interactable.actions && interactable.actions.length > 0 && (
|
||||||
<div className="interactable-actions">
|
<div className="interactable-actions">
|
||||||
@@ -231,10 +233,10 @@ function MovementControls({
|
|||||||
? 'Cannot interact during combat'
|
? 'Cannot interact during combat'
|
||||||
: cooldownRemaining > 0
|
: cooldownRemaining > 0
|
||||||
? `Wait ${cooldownRemaining}s`
|
? `Wait ${cooldownRemaining}s`
|
||||||
: action.description
|
: getTranslatedText(action.description)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{action.name}
|
{getTranslatedText(action.name)}
|
||||||
<span className="stamina-cost">
|
<span className="stamina-cost">
|
||||||
{cooldownRemaining > 0 ? `⏳${cooldownRemaining}s` : `⚡${action.stamina_cost}`}
|
{cooldownRemaining > 0 ? `⏳${cooldownRemaining}s` : `⚡${action.stamina_cost}`}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { PlayerState, Profile, Equipment } from './types'
|
import type { PlayerState, Profile, Equipment } from './types'
|
||||||
|
import { getAssetPath } from '../../utils/assetPath'
|
||||||
|
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||||
import InventoryModal from './InventoryModal'
|
import InventoryModal from './InventoryModal'
|
||||||
|
|
||||||
interface PlayerSidebarProps {
|
interface PlayerSidebarProps {
|
||||||
@@ -37,16 +40,18 @@ function PlayerSidebar({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const renderEquipmentSlot = (slot: string, item: any, emoji: string, label: string) => (
|
const renderEquipmentSlot = (slot: string, item: any, emoji: string, label: string) => (
|
||||||
<div className={`equipment-slot ${item ? 'filled' : 'empty'}`}>
|
<div className={`equipment-slot ${item ? 'filled' : 'empty'}`}>
|
||||||
{item ? (
|
{item ? (
|
||||||
<>
|
<>
|
||||||
<button className="equipment-unequip-btn" onClick={() => onUnequipItem(slot)} title="Unequip">✕</button>
|
<button className="equipment-unequip-btn" onClick={() => onUnequipItem(slot)} title={t('game.unequip')}>✕</button>
|
||||||
<div className="equipment-item-content">
|
<div className="equipment-item-content">
|
||||||
{item.image_path ? (
|
{item.image_path ? (
|
||||||
<img
|
<img
|
||||||
src={item.image_path}
|
src={getAssetPath(item.image_path)}
|
||||||
alt={item.name}
|
alt={getTranslatedText(item.name)}
|
||||||
className="equipment-emoji"
|
className="equipment-emoji"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
@@ -56,52 +61,52 @@ function PlayerSidebar({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<span className={`equipment-emoji ${item.image_path ? 'hidden' : ''}`}>{item.emoji}</span>
|
<span className={`equipment-emoji ${item.image_path ? 'hidden' : ''}`}>{item.emoji}</span>
|
||||||
<span className={`equipment-name ${item.tier ? `tier-${item.tier}` : ''}`}>{item.name}</span>
|
<span className={`equipment-name ${item.tier ? `tier-${item.tier}` : ''}`}>{getTranslatedText(item.name)}</span>
|
||||||
{item.durability && item.durability !== null && (
|
{item.durability && item.durability !== null && (
|
||||||
<span className="equipment-durability">{item.durability}/{item.max_durability}</span>
|
<span className="equipment-durability">{item.durability}/{item.max_durability}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="equipment-tooltip">
|
<div className="equipment-tooltip">
|
||||||
{item.description && <div className="item-tooltip-desc">{item.description}</div>}
|
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||||
{/* Use unique_stats if available, otherwise fall back to base stats */}
|
{/* Use unique_stats if available, otherwise fall back to base stats */}
|
||||||
{(item.unique_stats || item.stats) && Object.keys(item.unique_stats || item.stats).length > 0 && (
|
{(item.unique_stats || item.stats) && Object.keys(item.unique_stats || item.stats).length > 0 && (
|
||||||
<>
|
<>
|
||||||
{(item.unique_stats?.armor || item.stats?.armor) && (
|
{(item.unique_stats?.armor || item.stats?.armor) && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
🛡️ Armor: +{item.unique_stats?.armor || item.stats?.armor}
|
{t('stats.armor')}: +{item.unique_stats?.armor || item.stats?.armor}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(item.unique_stats?.hp_max || item.stats?.hp_max) && (
|
{(item.unique_stats?.hp_max || item.stats?.hp_max) && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
❤️ Max HP: +{item.unique_stats?.hp_max || item.stats?.hp_max}
|
{t('stats.hp')}: +{item.unique_stats?.hp_max || item.stats?.hp_max}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(item.unique_stats?.stamina_max || item.stats?.stamina_max) && (
|
{(item.unique_stats?.stamina_max || item.stats?.stamina_max) && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
⚡ Max Stamina: +{item.unique_stats?.stamina_max || item.stats?.stamina_max}
|
{t('stats.stamina')}: +{item.unique_stats?.stamina_max || item.stats?.stamina_max}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(item.unique_stats?.damage_min !== undefined || item.stats?.damage_min !== undefined) &&
|
{(item.unique_stats?.damage_min !== undefined || item.stats?.damage_min !== undefined) &&
|
||||||
(item.unique_stats?.damage_max !== undefined || item.stats?.damage_max !== undefined) && (
|
(item.unique_stats?.damage_max !== undefined || item.stats?.damage_max !== undefined) && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
⚔️ Damage: {item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max}
|
{t('stats.damage')}: {item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
|
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
⚖️ Weight: +{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg
|
{t('stats.weight')}: +{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
|
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
📦 Volume: +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L
|
{t('stats.volume')}: +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{item.durability !== undefined && item.durability !== null && (
|
{item.durability !== undefined && item.durability !== null && (
|
||||||
<div className="item-tooltip-stat">
|
<div className="item-tooltip-stat">
|
||||||
🔧 Durability: {item.durability}/{item.max_durability}
|
{t('stats.durability')}: {item.durability}/{item.max_durability}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
|
||||||
@@ -126,12 +131,12 @@ function PlayerSidebar({
|
|||||||
<div className={`right-sidebar mobile-menu-panel ${mobileMenuOpen === 'right' ? 'open' : ''}`}>
|
<div className={`right-sidebar mobile-menu-panel ${mobileMenuOpen === 'right' ? 'open' : ''}`}>
|
||||||
{/* Profile Stats */}
|
{/* Profile Stats */}
|
||||||
<div className="profile-sidebar">
|
<div className="profile-sidebar">
|
||||||
<h3>👤 Character</h3>
|
<h3>{t('game.character')}</h3>
|
||||||
|
|
||||||
<div className="sidebar-stat-bars">
|
<div className="sidebar-stat-bars">
|
||||||
<div className="sidebar-stat-bar">
|
<div className="sidebar-stat-bar">
|
||||||
<div className="sidebar-stat-header">
|
<div className="sidebar-stat-header">
|
||||||
<span className="sidebar-stat-label">❤️ HP</span>
|
<span className="sidebar-stat-label">{t('stats.hp')}</span>
|
||||||
<span className="sidebar-stat-numbers">{playerState.health}/{playerState.max_health}</span>
|
<span className="sidebar-stat-numbers">{playerState.health}/{playerState.max_health}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-progress-bar">
|
<div className="sidebar-progress-bar">
|
||||||
@@ -145,7 +150,7 @@ function PlayerSidebar({
|
|||||||
|
|
||||||
<div className="sidebar-stat-bar">
|
<div className="sidebar-stat-bar">
|
||||||
<div className="sidebar-stat-header">
|
<div className="sidebar-stat-header">
|
||||||
<span className="sidebar-stat-label">⚡ Stamina</span>
|
<span className="sidebar-stat-label">{t('stats.stamina')}</span>
|
||||||
<span className="sidebar-stat-numbers">{playerState.stamina}/{playerState.max_stamina}</span>
|
<span className="sidebar-stat-numbers">{playerState.stamina}/{playerState.max_stamina}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-progress-bar">
|
<div className="sidebar-progress-bar">
|
||||||
@@ -161,13 +166,13 @@ function PlayerSidebar({
|
|||||||
{profile && (
|
{profile && (
|
||||||
<div className="sidebar-stats">
|
<div className="sidebar-stats">
|
||||||
<div className="sidebar-stat-row">
|
<div className="sidebar-stat-row">
|
||||||
<span className="sidebar-label">Level:</span>
|
<span className="sidebar-label">{t('stats.level')}:</span>
|
||||||
<span className="sidebar-value">{profile.level}</span>
|
<span className="sidebar-value">{profile.level}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sidebar-stat-bar">
|
<div className="sidebar-stat-bar">
|
||||||
<div className="sidebar-stat-header">
|
<div className="sidebar-stat-header">
|
||||||
<span className="sidebar-stat-label">⭐ XP</span>
|
<span className="sidebar-stat-label">{t('stats.xp')}</span>
|
||||||
<span className="sidebar-stat-numbers">{profile.xp} / {(profile.level * 100)}</span>
|
<span className="sidebar-stat-numbers">{profile.xp} / {(profile.level * 100)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-progress-bar">
|
<div className="sidebar-progress-bar">
|
||||||
@@ -181,7 +186,7 @@ function PlayerSidebar({
|
|||||||
|
|
||||||
{profile.unspent_points > 0 && (
|
{profile.unspent_points > 0 && (
|
||||||
<div className="sidebar-stat-row highlight">
|
<div className="sidebar-stat-row highlight">
|
||||||
<span className="sidebar-label">⭐ Unspent:</span>
|
<span className="sidebar-label">{t('stats.unspentPoints')}:</span>
|
||||||
<span className="sidebar-value">{profile.unspent_points}</span>
|
<span className="sidebar-value">{profile.unspent_points}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -191,28 +196,28 @@ function PlayerSidebar({
|
|||||||
{/* Compact 2x2 Stats Grid */}
|
{/* Compact 2x2 Stats Grid */}
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="sidebar-stat-row compact">
|
<div className="sidebar-stat-row compact">
|
||||||
<span className="sidebar-label">💪 STR:</span>
|
<span className="sidebar-label">{t('stats.strength')}:</span>
|
||||||
<span className="sidebar-value">{profile.strength}</span>
|
<span className="sidebar-value">{profile.strength}</span>
|
||||||
{profile.unspent_points > 0 && (
|
{profile.unspent_points > 0 && (
|
||||||
<button className="stat-plus-btn" onClick={() => onSpendPoint('strength')}>+</button>
|
<button className="stat-plus-btn" onClick={() => onSpendPoint('strength')}>+</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-stat-row compact">
|
<div className="sidebar-stat-row compact">
|
||||||
<span className="sidebar-label">🏃 AGI:</span>
|
<span className="sidebar-label">{t('stats.agility')}:</span>
|
||||||
<span className="sidebar-value">{profile.agility}</span>
|
<span className="sidebar-value">{profile.agility}</span>
|
||||||
{profile.unspent_points > 0 && (
|
{profile.unspent_points > 0 && (
|
||||||
<button className="stat-plus-btn" onClick={() => onSpendPoint('agility')}>+</button>
|
<button className="stat-plus-btn" onClick={() => onSpendPoint('agility')}>+</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-stat-row compact">
|
<div className="sidebar-stat-row compact">
|
||||||
<span className="sidebar-label">🛡️ END:</span>
|
<span className="sidebar-label">{t('stats.endurance')}:</span>
|
||||||
<span className="sidebar-value">{profile.endurance}</span>
|
<span className="sidebar-value">{profile.endurance}</span>
|
||||||
{profile.unspent_points > 0 && (
|
{profile.unspent_points > 0 && (
|
||||||
<button className="stat-plus-btn" onClick={() => onSpendPoint('endurance')}>+</button>
|
<button className="stat-plus-btn" onClick={() => onSpendPoint('endurance')}>+</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-stat-row compact">
|
<div className="sidebar-stat-row compact">
|
||||||
<span className="sidebar-label">🧠 INT:</span>
|
<span className="sidebar-label">{t('stats.intellect')}:</span>
|
||||||
<span className="sidebar-value">{profile.intellect}</span>
|
<span className="sidebar-value">{profile.intellect}</span>
|
||||||
{profile.unspent_points > 0 && (
|
{profile.unspent_points > 0 && (
|
||||||
<button className="stat-plus-btn" onClick={() => onSpendPoint('intellect')}>+</button>
|
<button className="stat-plus-btn" onClick={() => onSpendPoint('intellect')}>+</button>
|
||||||
@@ -225,7 +230,7 @@ function PlayerSidebar({
|
|||||||
{/* Inventory Capacity - matching HP/Stamina/XP style */}
|
{/* Inventory Capacity - matching HP/Stamina/XP style */}
|
||||||
<div className="sidebar-stat-bar">
|
<div className="sidebar-stat-bar">
|
||||||
<div className="sidebar-stat-header">
|
<div className="sidebar-stat-header">
|
||||||
<span className="sidebar-stat-label">⚖️ Weight</span>
|
<span className="sidebar-stat-label">{t('stats.weight')}</span>
|
||||||
<span className="sidebar-stat-numbers">{(profile.current_weight || 0).toFixed(1)}/{profile.max_weight || 0}kg</span>
|
<span className="sidebar-stat-numbers">{(profile.current_weight || 0).toFixed(1)}/{profile.max_weight || 0}kg</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-progress-bar">
|
<div className="sidebar-progress-bar">
|
||||||
@@ -239,7 +244,7 @@ function PlayerSidebar({
|
|||||||
|
|
||||||
<div className="sidebar-stat-bar">
|
<div className="sidebar-stat-bar">
|
||||||
<div className="sidebar-stat-header">
|
<div className="sidebar-stat-header">
|
||||||
<span className="sidebar-stat-label">📦 Volume</span>
|
<span className="sidebar-stat-label">{t('stats.volume')}</span>
|
||||||
<span className="sidebar-stat-numbers">{(profile.current_volume || 0).toFixed(1)}/{profile.max_volume || 0}L</span>
|
<span className="sidebar-stat-numbers">{(profile.current_volume || 0).toFixed(1)}/{profile.max_volume || 0}L</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-progress-bar">
|
<div className="sidebar-progress-bar">
|
||||||
@@ -271,7 +276,7 @@ function PlayerSidebar({
|
|||||||
transition: 'all 0.2s'
|
transition: 'all 0.2s'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🎒 Open Inventory
|
{t('game.inventory')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -279,24 +284,24 @@ function PlayerSidebar({
|
|||||||
|
|
||||||
{/* Equipment Display - Proper Grid Layout */}
|
{/* Equipment Display - Proper Grid Layout */}
|
||||||
<div className="equipment-sidebar">
|
<div className="equipment-sidebar">
|
||||||
<h3>⚔️ Equipment</h3>
|
<h3>{t('game.equipment')}</h3>
|
||||||
<div className="equipment-grid">
|
<div className="equipment-grid">
|
||||||
{/* Row 1: Head */}
|
{/* Row 1: Head */}
|
||||||
<div className="equipment-row">
|
<div className="equipment-row">
|
||||||
{renderEquipmentSlot('head', equipment.head, '🪖', 'Head')}
|
{renderEquipmentSlot('head', equipment.head, '🪖', t('equipment.head'))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: Weapon, Torso, Backpack */}
|
{/* Row 2: Weapon, Torso, Backpack */}
|
||||||
<div className="equipment-row three-cols">
|
<div className="equipment-row three-cols">
|
||||||
{renderEquipmentSlot('weapon', equipment.weapon, '⚔️', 'Weapon')}
|
{renderEquipmentSlot('weapon', equipment.weapon, '⚔️', t('equipment.weapon'))}
|
||||||
{renderEquipmentSlot('torso', equipment.torso, '👕', 'Torso')}
|
{renderEquipmentSlot('torso', equipment.torso, '👕', t('equipment.torso'))}
|
||||||
{renderEquipmentSlot('backpack', equipment.backpack, '🎒', 'Backpack')}
|
{renderEquipmentSlot('backpack', equipment.backpack, '🎒', t('equipment.backpack'))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 3: Legs & Feet */}
|
{/* Row 3: Legs & Feet */}
|
||||||
<div className="equipment-row two-cols">
|
<div className="equipment-row two-cols">
|
||||||
{renderEquipmentSlot('legs', equipment.legs, '👖', 'Legs')}
|
{renderEquipmentSlot('legs', equipment.legs, '👖', t('equipment.legs'))}
|
||||||
{renderEquipmentSlot('feet', equipment.feet, '👟', 'Feet')}
|
{renderEquipmentSlot('feet', equipment.feet, '👟', t('equipment.feet'))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useState, useEffect, type MouseEvent, type ChangeEvent } from 'react'
|
import { useState, useEffect, type MouseEvent, type ChangeEvent } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { Profile, WorkbenchTab } from './types'
|
import type { Profile, WorkbenchTab } from './types'
|
||||||
|
import { getAssetPath } from '../../utils/assetPath'
|
||||||
|
import { getTranslatedText } from '../../utils/i18nUtils'
|
||||||
import './Workbench.css'
|
import './Workbench.css'
|
||||||
|
|
||||||
interface WorkbenchProps {
|
interface WorkbenchProps {
|
||||||
@@ -47,6 +50,8 @@ function Workbench({
|
|||||||
onRepair,
|
onRepair,
|
||||||
onUncraft
|
onUncraft
|
||||||
}: WorkbenchProps) {
|
}: WorkbenchProps) {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
const [selectedItem, setSelectedItem] = useState<any>(null)
|
const [selectedItem, setSelectedItem] = useState<any>(null)
|
||||||
|
|
||||||
// Reset selection when tab changes
|
// Reset selection when tab changes
|
||||||
@@ -84,12 +89,12 @@ function Workbench({
|
|||||||
switch (workbenchTab) {
|
switch (workbenchTab) {
|
||||||
case 'craft':
|
case 'craft':
|
||||||
return craftableItems.filter(item =>
|
return craftableItems.filter(item =>
|
||||||
item.name.toLowerCase().includes(craftFilter.toLowerCase()) &&
|
getTranslatedText(item.name).toLowerCase().includes(craftFilter.toLowerCase()) &&
|
||||||
(craftCategoryFilter === 'all' || item.category === craftCategoryFilter)
|
(craftCategoryFilter === 'all' || item.category === craftCategoryFilter)
|
||||||
)
|
)
|
||||||
case 'repair':
|
case 'repair':
|
||||||
return repairableItems
|
return repairableItems
|
||||||
.filter(item => item.name.toLowerCase().includes(repairFilter.toLowerCase()))
|
.filter(item => getTranslatedText(item.name).toLowerCase().includes(repairFilter.toLowerCase()))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.needs_repair && !b.needs_repair) return -1
|
if (a.needs_repair && !b.needs_repair) return -1
|
||||||
if (!a.needs_repair && b.needs_repair) return 1
|
if (!a.needs_repair && b.needs_repair) return 1
|
||||||
@@ -97,7 +102,7 @@ function Workbench({
|
|||||||
})
|
})
|
||||||
case 'uncraft':
|
case 'uncraft':
|
||||||
return uncraftableItems.filter(item =>
|
return uncraftableItems.filter(item =>
|
||||||
item.name.toLowerCase().includes(uncraftFilter.toLowerCase())
|
getTranslatedText(item.name).toLowerCase().includes(uncraftFilter.toLowerCase())
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
@@ -118,7 +123,7 @@ function Workbench({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const item = selectedItem
|
const item = selectedItem
|
||||||
const imagePath = item.image_path || `/images/items/${item.item_id || item.id}.webp`
|
const imagePath = getAssetPath(item.image_path || `images/items/${item.item_id || item.id}.webp`)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -127,7 +132,7 @@ function Workbench({
|
|||||||
{imagePath ? (
|
{imagePath ? (
|
||||||
<img
|
<img
|
||||||
src={imagePath}
|
src={imagePath}
|
||||||
alt={item.name}
|
alt={getTranslatedText(item.name)}
|
||||||
className="detail-image"
|
className="detail-image"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
@@ -140,8 +145,9 @@ function Workbench({
|
|||||||
{item.emoji || '📦'}
|
{item.emoji || '📦'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="detail-title">{item.emoji} {item.name}</h2>
|
<div className="item-detail-header">
|
||||||
{item.description && <p className="detail-description">{item.description}</p>}
|
<h2 className="detail-title">{item.emoji} {getTranslatedText(item.name)}</h2>
|
||||||
|
{item.description && <p className="detail-description">{getTranslatedText(item.description)}</p>}
|
||||||
|
|
||||||
{/* Base Stats Display for Crafting */}
|
{/* Base Stats Display for Crafting */}
|
||||||
{workbenchTab === 'craft' && (item.base_stats || item.stats) && (
|
{workbenchTab === 'craft' && (item.base_stats || item.stats) && (
|
||||||
@@ -214,7 +220,7 @@ function Workbench({
|
|||||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
|
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
|
||||||
{item.tools.map((tool: any, i: number) => (
|
{item.tools.map((tool: any, i: number) => (
|
||||||
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
||||||
<span>{tool.emoji} {tool.name}</span>
|
<span>{tool.emoji} {getTranslatedText(tool.name)}</span>
|
||||||
<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} (Cost: ${tool.durability_cost})` : `❌ Missing (Cost: ${tool.durability_cost})`}
|
||||||
</span>
|
</span>
|
||||||
@@ -227,7 +233,7 @@ function Workbench({
|
|||||||
{item.materials && item.materials.length > 0 ? (
|
{item.materials && item.materials.length > 0 ? (
|
||||||
item.materials.map((mat: any, i: number) => (
|
item.materials.map((mat: any, i: number) => (
|
||||||
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
||||||
<span>{mat.emoji} {mat.name}</span>
|
<span>{mat.emoji} {getTranslatedText(mat.name)}</span>
|
||||||
<span>{mat.available} / {mat.required}</span>
|
<span>{mat.available} / {mat.required}</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -299,7 +305,7 @@ function Workbench({
|
|||||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
|
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
|
||||||
{item.tools.map((tool: any, i: number) => (
|
{item.tools.map((tool: any, i: number) => (
|
||||||
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
||||||
<span>{tool.emoji} {tool.name}</span>
|
<span>{tool.emoji} {getTranslatedText(tool.name)}</span>
|
||||||
<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} (Cost: ${tool.durability_cost})` : `❌ Missing (Cost: ${tool.durability_cost})`}
|
||||||
</span>
|
</span>
|
||||||
@@ -311,7 +317,7 @@ function Workbench({
|
|||||||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Materials</h5>
|
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Materials</h5>
|
||||||
{item.materials.map((mat: any, i: number) => (
|
{item.materials.map((mat: any, i: number) => (
|
||||||
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
||||||
<span>{mat.emoji} {mat.name}</span>
|
<span>{mat.emoji} {getTranslatedText(mat.name)}</span>
|
||||||
<span>{mat.available} / {mat.quantity}</span>
|
<span>{mat.available} / {mat.quantity}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -388,7 +394,7 @@ function Workbench({
|
|||||||
|
|
||||||
{adjustedYield.map((mat: any, i: number) => (
|
{adjustedYield.map((mat: any, i: number) => (
|
||||||
<div key={i} className="requirement-item met">
|
<div key={i} className="requirement-item met">
|
||||||
<span>{mat.emoji} {mat.name}</span>
|
<span>{mat.emoji} {getTranslatedText(mat.name)}</span>
|
||||||
<span>x{mat.adjusted_quantity}</span>
|
<span>x{mat.adjusted_quantity}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -403,7 +409,7 @@ function Workbench({
|
|||||||
className="uncraft-btn"
|
className="uncraft-btn"
|
||||||
disabled={(profile?.stamina || 0) < (item.stamina_cost || 1)}
|
disabled={(profile?.stamina || 0) < (item.stamina_cost || 1)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm(`Are you sure you want to salvage ${item.name}? This cannot be undone.`)) {
|
if (window.confirm(`Are you sure you want to salvage ${getTranslatedText(item.name)}? This cannot be undone.`)) {
|
||||||
onUncraft(item.unique_item_id, item.inventory_id)
|
onUncraft(item.unique_item_id, item.inventory_id)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -417,6 +423,7 @@ function Workbench({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -500,7 +507,7 @@ function Workbench({
|
|||||||
{items.filter(item => {
|
{items.filter(item => {
|
||||||
// Text search filter
|
// Text search filter
|
||||||
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
||||||
const matchesSearch = !searchFilter || item.name.toLowerCase().includes(searchFilter.toLowerCase())
|
const matchesSearch = !searchFilter || getTranslatedText(item.name).toLowerCase().includes(searchFilter.toLowerCase())
|
||||||
|
|
||||||
// Category filter (apply to all tabs)
|
// Category filter (apply to all tabs)
|
||||||
let matchesCategory = true
|
let matchesCategory = true
|
||||||
@@ -521,7 +528,7 @@ function Workbench({
|
|||||||
.filter(item => {
|
.filter(item => {
|
||||||
// Text search filter
|
// Text search filter
|
||||||
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
||||||
const matchesSearch = !searchFilter || item.name.toLowerCase().includes(searchFilter.toLowerCase())
|
const matchesSearch = !searchFilter || getTranslatedText(item.name).toLowerCase().includes(searchFilter.toLowerCase())
|
||||||
|
|
||||||
// Category filter (apply to all tabs)
|
// Category filter (apply to all tabs)
|
||||||
let matchesCategory = true
|
let matchesCategory = true
|
||||||
@@ -533,7 +540,7 @@ function Workbench({
|
|||||||
return matchesSearch && matchesCategory
|
return matchesSearch && matchesCategory
|
||||||
})
|
})
|
||||||
.map((item: any, idx: number) => {
|
.map((item: any, idx: number) => {
|
||||||
const imagePath = item.image_path || `/images/items/${item.item_id || item.id}.webp`
|
const imagePath = getAssetPath(item.image_path || `images/items/${item.item_id || item.id}.webp`)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.unique_item_id || item.item_id || idx}
|
key={item.unique_item_id || item.item_id || idx}
|
||||||
@@ -545,7 +552,7 @@ function Workbench({
|
|||||||
{imagePath ? (
|
{imagePath ? (
|
||||||
<img
|
<img
|
||||||
src={imagePath}
|
src={imagePath}
|
||||||
alt={item.name}
|
alt={getTranslatedText(item.name)}
|
||||||
className="item-thumb-img"
|
className="item-thumb-img"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
@@ -564,7 +571,7 @@ function Workbench({
|
|||||||
<span
|
<span
|
||||||
className={`item-name ${(workbenchTab === 'repair' || workbenchTab === 'uncraft') && item.tier ? `text-tier-${item.tier}` : ''}`}
|
className={`item-name ${(workbenchTab === 'repair' || workbenchTab === 'uncraft') && item.tier ? `text-tier-${item.tier}` : ''}`}
|
||||||
>
|
>
|
||||||
{item.name}
|
{getTranslatedText(item.name)}
|
||||||
</span>
|
</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' }}>Equipped</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ export interface DirectionDetail {
|
|||||||
stamina_cost: number
|
stamina_cost: number
|
||||||
distance: number
|
distance: number
|
||||||
destination: string
|
destination: string
|
||||||
destination_name?: string
|
destination_name?: string | { [key: string]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Location {
|
export interface Location {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string | { [key: string]: string }
|
||||||
description: string
|
description: string | { [key: string]: string }
|
||||||
directions: string[]
|
directions: string[]
|
||||||
directions_detailed?: DirectionDetail[]
|
directions_detailed?: DirectionDetail[]
|
||||||
danger_level?: number
|
danger_level?: number
|
||||||
|
|||||||
29
pwa/src/i18n/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
|
||||||
|
import en from './locales/en.json'
|
||||||
|
import es from './locales/es.json'
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: { translation: en },
|
||||||
|
es: { translation: es }
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
supportedLngs: ['en', 'es'],
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false // React already escapes
|
||||||
|
},
|
||||||
|
detection: {
|
||||||
|
order: ['localStorage', 'navigator'],
|
||||||
|
caches: ['localStorage']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
136
pwa/src/i18n/locales/en.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "Error",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"close": "Close",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"game": "Game",
|
||||||
|
"leaderboards": "Leaderboards",
|
||||||
|
"account": "Account"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Login",
|
||||||
|
"logout": "Logout",
|
||||||
|
"register": "Register",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"email": "Email",
|
||||||
|
"forgotPassword": "Forgot Password?",
|
||||||
|
"createAccount": "Create Account",
|
||||||
|
"alreadyHaveAccount": "Already have an account?",
|
||||||
|
"dontHaveAccount": "Don't have an account?"
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"travel": "🧭 Travel",
|
||||||
|
"surroundings": "🌿 Surroundings",
|
||||||
|
"character": "👤 Character",
|
||||||
|
"equipment": "⚔️ Equipment",
|
||||||
|
"inventory": "🎒 Open Inventory",
|
||||||
|
"workbench": "🔧 Workbench",
|
||||||
|
"craft": "🔨 Craft",
|
||||||
|
"repair": "🛠️ Repair",
|
||||||
|
"salvage": "♻️ Salvage",
|
||||||
|
"pickUp": "Pick Up",
|
||||||
|
"drop": "Drop",
|
||||||
|
"dropAll": "All",
|
||||||
|
"use": "Use",
|
||||||
|
"equip": "Equip",
|
||||||
|
"unequip": "Unequip",
|
||||||
|
"attack": "Attack",
|
||||||
|
"flee": "Flee",
|
||||||
|
"rest": "Rest",
|
||||||
|
"onlineCount": "{{count}} Online"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"hp": "❤️ HP",
|
||||||
|
"maxHp": "❤️ Max HP",
|
||||||
|
"stamina": "⚡ Stamina",
|
||||||
|
"maxStamina": "⚡ Max Stamina",
|
||||||
|
"xp": "⭐ XP",
|
||||||
|
"level": "Level",
|
||||||
|
"unspentPoints": "⭐ Unspent",
|
||||||
|
"weight": "⚖️ Weight",
|
||||||
|
"volume": "📦 Volume",
|
||||||
|
"strength": "💪 STR",
|
||||||
|
"strengthFull": "Strength",
|
||||||
|
"strengthDesc": "Increases melee damage and carry capacity",
|
||||||
|
"agility": "🏃 AGI",
|
||||||
|
"agilityFull": "Agility",
|
||||||
|
"agilityDesc": "Improves dodge chance and critical hits",
|
||||||
|
"endurance": "🛡️ END",
|
||||||
|
"enduranceFull": "Endurance",
|
||||||
|
"enduranceDesc": "Increases HP and stamina",
|
||||||
|
"intellect": "🧠 INT",
|
||||||
|
"intellectFull": "Intellect",
|
||||||
|
"intellectDesc": "Enhances crafting and resource gathering",
|
||||||
|
"armor": "🛡️ Armor",
|
||||||
|
"damage": "⚔️ Damage",
|
||||||
|
"durability": "Durability"
|
||||||
|
},
|
||||||
|
"combat": {
|
||||||
|
"inCombat": "In Combat",
|
||||||
|
"yourTurn": "Your Turn",
|
||||||
|
"enemyTurn": "Enemy's Turn",
|
||||||
|
"victory": "Victory!",
|
||||||
|
"defeat": "Defeat",
|
||||||
|
"youDied": "You Died",
|
||||||
|
"respawn": "Respawn",
|
||||||
|
"fleeSuccess": "You escaped!",
|
||||||
|
"fleeFailed": "Failed to escape!"
|
||||||
|
},
|
||||||
|
"equipment": {
|
||||||
|
"head": "Head",
|
||||||
|
"torso": "Torso",
|
||||||
|
"legs": "Legs",
|
||||||
|
"feet": "Feet",
|
||||||
|
"weapon": "Weapon",
|
||||||
|
"backpack": "Backpack",
|
||||||
|
"noBackpack": "No Backpack Equipped",
|
||||||
|
"equipped": "Equipped"
|
||||||
|
},
|
||||||
|
"crafting": {
|
||||||
|
"requirements": "📊 Requirements",
|
||||||
|
"materials": "Materials",
|
||||||
|
"tools": "Tools",
|
||||||
|
"levelRequired": "Level {{level}} Required",
|
||||||
|
"missingRequirements": "Missing Requirements",
|
||||||
|
"craftItem": "🔨 Craft Item",
|
||||||
|
"repairItem": "🛠️ Repair Item",
|
||||||
|
"salvageItem": "♻️ Salvage Item",
|
||||||
|
"staminaCost": "⚡ {{cost}} Stamina",
|
||||||
|
"alreadyFull": "Already Full",
|
||||||
|
"perfectCondition": "✅ Item is in perfect condition",
|
||||||
|
"yieldReduced": "⚠️ Yield reduced by {{percent}}% due to damage"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"all": "All Items",
|
||||||
|
"weapon": "Weapons",
|
||||||
|
"armor": "Armor",
|
||||||
|
"clothing": "Clothing",
|
||||||
|
"backpack": "Backpacks",
|
||||||
|
"tool": "Tools",
|
||||||
|
"consumable": "Consumables",
|
||||||
|
"resource": "Resources",
|
||||||
|
"quest": "Quest",
|
||||||
|
"misc": "Misc"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"notEnoughStamina": "Not enough stamina",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"heroTitle": "Echoes of the Ash",
|
||||||
|
"heroSubtitle": "A post-apocalyptic survival RPG",
|
||||||
|
"playNow": "Play Now",
|
||||||
|
"features": "Features"
|
||||||
|
}
|
||||||
|
}
|
||||||
136
pwa/src/i18n/locales/es.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"error": "Error",
|
||||||
|
"save": "Guardar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"yes": "Sí",
|
||||||
|
"no": "No",
|
||||||
|
"game": "Juego",
|
||||||
|
"leaderboards": "Clasificación",
|
||||||
|
"account": "Cuenta"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Iniciar Sesión",
|
||||||
|
"logout": "Cerrar Sesión",
|
||||||
|
"register": "Registrarse",
|
||||||
|
"username": "Usuario",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"email": "Correo",
|
||||||
|
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||||
|
"createAccount": "Crear Cuenta",
|
||||||
|
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
||||||
|
"dontHaveAccount": "¿No tienes cuenta?"
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"travel": "🧭 Viajar",
|
||||||
|
"surroundings": "🌿 Alrededores",
|
||||||
|
"character": "👤 Personaje",
|
||||||
|
"equipment": "⚔️ Equipamiento",
|
||||||
|
"inventory": "🎒 Abrir Inventario",
|
||||||
|
"workbench": "🔧 Banco de Trabajo",
|
||||||
|
"craft": "🔨 Fabricar",
|
||||||
|
"repair": "🛠️ Reparar",
|
||||||
|
"salvage": "♻️ Desmontar",
|
||||||
|
"pickUp": "Recoger",
|
||||||
|
"drop": "Soltar",
|
||||||
|
"dropAll": "Todo",
|
||||||
|
"use": "Usar",
|
||||||
|
"equip": "Equipar",
|
||||||
|
"unequip": "Desequipar",
|
||||||
|
"attack": "Atacar",
|
||||||
|
"flee": "Huir",
|
||||||
|
"rest": "Descansar",
|
||||||
|
"onlineCount": "{{count}} En línea"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"hp": "❤️ Vida",
|
||||||
|
"maxHp": "❤️ Vida Máx.",
|
||||||
|
"stamina": "⚡ Aguante",
|
||||||
|
"maxStamina": "⚡ Aguante Máx.",
|
||||||
|
"xp": "⭐ XP",
|
||||||
|
"level": "Nivel",
|
||||||
|
"unspentPoints": "⭐ Sin gastar",
|
||||||
|
"weight": "⚖️ Peso",
|
||||||
|
"volume": "📦 Volumen",
|
||||||
|
"strength": "💪 FUE",
|
||||||
|
"strengthFull": "Fuerza",
|
||||||
|
"strengthDesc": "Aumenta el daño cuerpo a cuerpo y capacidad de carga",
|
||||||
|
"agility": "🏃 AGI",
|
||||||
|
"agilityFull": "Agilidad",
|
||||||
|
"agilityDesc": "Mejora la esquiva y golpes críticos",
|
||||||
|
"endurance": "🛡️ RES",
|
||||||
|
"enduranceFull": "Resistencia",
|
||||||
|
"enduranceDesc": "Aumenta la vida y energía",
|
||||||
|
"intellect": "🧠 INT",
|
||||||
|
"intellectFull": "Intelecto",
|
||||||
|
"intellectDesc": "Mejora la fabricación y recolección",
|
||||||
|
"armor": "🛡️ Armadura",
|
||||||
|
"damage": "⚔️ Daño",
|
||||||
|
"durability": "Durabilidad"
|
||||||
|
},
|
||||||
|
"combat": {
|
||||||
|
"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!"
|
||||||
|
},
|
||||||
|
"equipment": {
|
||||||
|
"head": "Cabeza",
|
||||||
|
"torso": "Torso",
|
||||||
|
"legs": "Piernas",
|
||||||
|
"feet": "Pies",
|
||||||
|
"weapon": "Arma",
|
||||||
|
"backpack": "Mochila",
|
||||||
|
"noBackpack": "Sin Mochila Equipada",
|
||||||
|
"equipped": "Equipado"
|
||||||
|
},
|
||||||
|
"crafting": {
|
||||||
|
"requirements": "📊 Requisitos",
|
||||||
|
"materials": "Materiales",
|
||||||
|
"tools": "Herramientas",
|
||||||
|
"levelRequired": "Nivel {{level}} Requerido",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"all": "Todos",
|
||||||
|
"weapon": "Armas",
|
||||||
|
"armor": "Armadura",
|
||||||
|
"clothing": "Ropa",
|
||||||
|
"backpack": "Mochilas",
|
||||||
|
"tool": "Herramientas",
|
||||||
|
"consumable": "Consumibles",
|
||||||
|
"resource": "Recursos",
|
||||||
|
"quest": "Misión",
|
||||||
|
"misc": "Varios"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"notEnoughStamina": "No tienes suficiente energía",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"heroTitle": "Ecos de la Ceniza",
|
||||||
|
"heroSubtitle": "Un RPG de supervivencia post-apocalíptico",
|
||||||
|
"playNow": "Jugar Ahora",
|
||||||
|
"features": "Características"
|
||||||
|
}
|
||||||
|
}
|
||||||