Release v0.2.10: Update package-lock.json and CI config

This commit is contained in:
Joan
2025-12-30 18:51:21 +01:00
parent 8b31011334
commit 592f38827e
108 changed files with 2755 additions and 1112 deletions

View File

@@ -30,48 +30,48 @@ build:web:
tags:
- docker
# Build Linux AppImage and .deb
build:linux:
stage: build-desktop
image: electronuserland/builder:wine
dependencies:
- build:web
script:
- cd pwa
- npm ci
- npm run electron:build:linux
- echo "=== AppImage size ==="
- ls -lh dist-electron/*.AppImage
- du -h dist-electron/*.AppImage
artifacts:
paths:
- pwa/dist-electron/*.AppImage
expire_in: 1 week
name: "linux-appimage-$CI_COMMIT_TAG"
rules:
- if: '$CI_COMMIT_TAG'
tags:
- docker
# # Build Linux AppImage and .deb
# build:linux:
# stage: build-desktop
# image: electronuserland/builder:wine
# dependencies:
# - build:web
# script:
# - cd pwa
# - npm ci
# - npm run electron:build:linux
# - echo "=== AppImage size ==="
# - ls -lh dist-electron/*.AppImage
# - du -h dist-electron/*.AppImage
# artifacts:
# paths:
# - pwa/dist-electron/*.AppImage
# expire_in: 1 week
# name: "linux-appimage-$CI_COMMIT_TAG"
# rules:
# - if: '$CI_COMMIT_TAG'
# tags:
# - docker
# Build Linux .deb (separate job to avoid size limits)
build:linux-deb:
stage: build-desktop
image: electronuserland/builder:wine
dependencies:
- build:web
script:
- cd pwa
- npm ci
- npm run electron:build:linux
artifacts:
paths:
- pwa/dist-electron/*.deb
expire_in: 1 week
name: "linux-deb-$CI_COMMIT_TAG"
rules:
- if: '$CI_COMMIT_TAG'
tags:
- docker
# # Build Linux .deb (separate job to avoid size limits)
# build:linux-deb:
# stage: build-desktop
# image: electronuserland/builder:wine
# dependencies:
# - build:web
# script:
# - cd pwa
# - npm ci
# - npm run electron:build:linux
# artifacts:
# paths:
# - pwa/dist-electron/*.deb
# expire_in: 1 week
# name: "linux-deb-$CI_COMMIT_TAG"
# rules:
# - if: '$CI_COMMIT_TAG'
# tags:
# - docker
# Build Windows executable
build:windows:

View File

@@ -4,7 +4,7 @@ Loads and manages game items from JSON without bot dependencies.
"""
import json
from pathlib import Path
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, Union
from dataclasses import dataclass
@@ -12,8 +12,8 @@ from dataclasses import dataclass
class Item:
"""Represents a game item"""
id: str
name: str
description: str
name: Union[str, Dict[str, str]]
description: Union[str, Dict[str, str]]
type: str
image_path: str = ""
emoji: str = "📦"

View File

@@ -12,7 +12,7 @@ import logging
from ..core.security import get_current_user, security, verify_internal_key
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 ..items import ItemsManager
from .. import game_logic
@@ -226,6 +226,11 @@ async def get_game_state(current_user: dict = Depends(get_current_user)):
"encumbrance": item_def.encumbrance,
"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:
equipment[slot] = None

View File

@@ -3,11 +3,18 @@ Helper utilities for game calculations and common operations.
Contains distance calculations, stamina costs, capacity calculations, etc.
"""
import math
from typing import Tuple, List, Dict, Any
from typing import Tuple, List, Dict, Any, Union
from .. import database as db
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:
"""
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.
broken_armor.append({
'name': armor['item_def'].name,
'name': get_locale_string(armor['item_def'].name),
'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'],
'item_id': inv_item['item_id'],
'durability': unique_item['durability'],
'name': item_def.name,
'name': get_locale_string(item_def.name),
'emoji': getattr(item_def, 'emoji', '🔧')
})

View File

@@ -4,14 +4,14 @@ Loads game data from JSON files without bot dependencies.
"""
import json
from pathlib import Path
from typing import Dict, List, Any, Optional
from typing import Dict, List, Any, Optional, Union
from dataclasses import dataclass, field
@dataclass
class Outcome:
"""Represents an outcome of an action"""
text: str
text: Union[str, Dict[str, str]]
items_reward: Dict[str, int] = field(default_factory=dict)
damage_taken: int = 0
@@ -20,7 +20,7 @@ class Outcome:
class Action:
"""Represents an action that can be performed on an interactable"""
id: str
label: str
label: Union[str, Dict[str, str]]
stamina_cost: int = 2
outcomes: Dict[str, Outcome] = field(default_factory=dict)
@@ -32,7 +32,7 @@ class Action:
class Interactable:
"""Represents an interactable object"""
id: str
name: str
name: Union[str, Dict[str, str]]
image_path: str = ""
actions: List[Action] = field(default_factory=list)
@@ -52,8 +52,8 @@ class Exit:
class Location:
"""Represents a location in the game world"""
id: str
name: str
description: str
name: Union[str, Dict[str, str]]
description: Union[str, Dict[str, str]]
image_path: str = ""
exits: Dict[str, str] = field(default_factory=dict) # direction -> destination_id
exit_stamina: Dict[str, int] = field(default_factory=dict) # direction -> stamina_cost

View File

@@ -2,114 +2,192 @@
"interactables": {
"rubble": {
"id": "rubble",
"name": "🧱 Pile of Rubble",
"description": "A scattered pile of debris and broken concrete.",
"name": {
"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",
"actions": {
"search": {
"id": "search",
"label": "\ud83d\udd0e Search Rubble",
"label": {
"en": "🔎 Search Rubble",
"es": "🔎 Buscar en los escombros"
},
"stamina_cost": 2
}
}
},
"dumpster": {
"id": "dumpster",
"name": "\ud83d\uddd1\ufe0f Dumpster",
"description": "A rusted metal dumpster, possibly containing scavenged goods.",
"name": {
"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",
"actions": {
"search_dumpster": {
"id": "search_dumpster",
"label": "\ud83d\udd0e Dig Through Trash",
"label": {
"en": "🔎 Dig Through Trash",
"es": "🔎 Buscar en la basura"
},
"stamina_cost": 2
}
}
},
"sedan": {
"id": "sedan",
"name": "\ud83d\ude97 Rusty Sedan",
"description": "An abandoned sedan with rusted doors.",
"name": {
"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",
"actions": {
"search_glovebox": {
"id": "search_glovebox",
"label": "\ud83d\udd0e Search Glovebox",
"label": {
"en": "🔎 Search Glovebox",
"es": "🔎 Buscar en la guantera"
},
"stamina_cost": 1
},
"pop_trunk": {
"id": "pop_trunk",
"label": "\ud83d\udd27 Pop the Trunk",
"label": {
"en": "🔧 Pop the Trunk",
"es": "🔧 Forzar el maletero"
},
"stamina_cost": 3
}
}
},
"house": {
"id": "house",
"name": "\ud83c\udfda\ufe0f Abandoned House",
"description": "A dilapidated house with boarded windows.",
"name": {
"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",
"actions": {
"search_house": {
"id": "search_house",
"label": "\ud83d\udd0e Search House",
"label": {
"en": "🔎 Search House",
"es": "🔎 Buscar en la casa"
},
"stamina_cost": 3
}
}
},
"toolshed": {
"id": "toolshed",
"name": "\ud83d\udd28 Tool Shed",
"description": "A small wooden shed, door slightly ajar.",
"name": {
"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",
"actions": {
"search_shed": {
"id": "search_shed",
"label": "\ud83d\udd0e Search Shed",
"label": {
"en": "🔎 Search Shed",
"es": "🔎 Buscar en el almacén"
},
"stamina_cost": 2
}
}
},
"medkit": {
"id": "medkit",
"name": "\ud83c\udfe5 Medical Supply Cabinet",
"description": "A white metal cabinet with a red cross symbol.",
"name": {
"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",
"actions": {
"search_medkit": {
"id": "search_medkit",
"label": "\ud83d\udd0e Search Cabinet",
"label": {
"en": "🔎 Search Cabinet",
"es": "🔎 Buscar en el armario"
},
"stamina_cost": 2
}
}
},
"storage_box": {
"id": "storage_box",
"name": "📦 Storage Box",
"description": "A weathered storage container.",
"name": {
"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",
"actions": {
"search": {
"id": "search",
"label": "\ud83d\udd0e Search Box",
"label": {
"en": "🔎 Search Box",
"es": "🔎 Buscar en la caja"
},
"stamina_cost": 2
}
}
},
"vending_machine": {
"id": "vending_machine",
"name": "\ud83e\uddc3 Vending Machine",
"description": "A broken vending machine, glass shattered.",
"name": {
"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",
"actions": {
"break": {
"id": "break",
"label": "\ud83d\udd28 Break Open",
"label": {
"en": "🔨 Break Open",
"es": "🔨 Forzar la máquina"
},
"stamina_cost": 5
},
"search": {
"id": "search",
"label": "\ud83d\udd0e Search Machine",
"label": {
"en": "🔎 Search Machine",
"es": "🔎 Buscar en la máquina"
},
"stamina_cost": 2
}
}

View File

@@ -1,48 +1,78 @@
{
"items": {
"scrap_metal": {
"name": "Scrap Metal",
"name": {
"en": "Scrap Metal",
"es": "Metal desechado"
},
"type": "resource",
"weight": 0.5,
"volume": 0.2,
"emoji": "\u2699\ufe0f",
"emoji": "⚙️",
"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": {
"name": "Rusty Nails",
"name": {
"en": "Rusty Nails",
"es": "Clavos oxidados"
},
"weight": 0.2,
"volume": 0.1,
"type": "resource",
"emoji": "\ud83d\udccc",
"emoji": "📌",
"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": {
"name": "Wood Planks",
"name": {
"en": "Wood Planks",
"es": "Tablillas de madera"
},
"weight": 3.0,
"volume": 2.0,
"type": "resource",
"emoji": "\ud83e\udeb5",
"emoji": "🪵",
"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": {
"name": "Cloth Scraps",
"name": {
"en": "Cloth Scraps",
"es": "Ramas de tela"
},
"weight": 0.1,
"volume": 0.2,
"type": "resource",
"emoji": "\ud83e\uddf5",
"emoji": "🧵",
"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": {
"name": "Cloth",
"name": {
"en": "Cloth",
"es": "Tela"
},
"type": "resource",
"weight": 0.1,
"volume": 0.2,
"emoji": "\ud83e\uddf5",
"description": "A raw material used for crafting and upgrades.",
"emoji": "🧵",
"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",
"uncraftable": true,
"uncraft_yield": [
@@ -59,187 +89,301 @@
]
},
"plastic_bottles": {
"name": "Plastic Bottles",
"name": {
"en": "Plastic Bottles",
"es": "Botellas de plástico"
},
"weight": 0.05,
"volume": 0.3,
"type": "resource",
"emoji": "\ud83c\udf76",
"emoji": "🍶",
"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": {
"name": "Bone",
"name": {
"en": "Bone",
"es": "Hueso"
},
"weight": 0.3,
"volume": 0.1,
"type": "resource",
"emoji": "\ud83e\uddb4",
"emoji": "🦴",
"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": {
"name": "Raw Meat",
"name": {
"en": "Raw Meat",
"es": "Carne cruda"
},
"weight": 0.5,
"volume": 0.2,
"type": "resource",
"emoji": "\ud83e\udd69",
"emoji": "🥩",
"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": {
"name": "Animal Hide",
"name": {
"en": "Animal Hide",
"es": "Piel de animal"
},
"weight": 0.4,
"volume": 0.3,
"type": "resource",
"emoji": "\ud83e\udde4",
"emoji": "🧤",
"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": {
"name": "Mutant Tissue",
"name": {
"en": "Mutant Tissue",
"es": "Piel de mutante"
},
"weight": 0.2,
"volume": 0.1,
"type": "resource",
"emoji": "\ud83e\uddec",
"emoji": "🧬",
"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": {
"name": "Infected Tissue",
"name": {
"en": "Infected Tissue",
"es": "Piel infectada"
},
"weight": 0.2,
"volume": 0.1,
"type": "resource",
"emoji": "\u2623\ufe0f",
"emoji": "☣️",
"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": {
"name": "Stale Chocolate Bar",
"name": {
"en": "Stale Chocolate Bar",
"es": "Barra de chocolate caducada"
},
"weight": 0.1,
"volume": 0.1,
"type": "consumable",
"hp_restore": 10,
"emoji": "\ud83c\udf6b",
"emoji": "🍫",
"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": {
"name": "Canned Beans",
"name": {
"en": "Canned Beans",
"es": "Frijoles enlatados"
},
"weight": 0.4,
"volume": 0.2,
"type": "consumable",
"hp_restore": 20,
"stamina_restore": 5,
"emoji": "\ud83e\udd6b",
"emoji": "🥫",
"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": {
"name": "Canned Food",
"name": {
"en": "Canned Food",
"es": "Comida enlatada"
},
"weight": 0.4,
"volume": 0.2,
"type": "consumable",
"hp_restore": 25,
"stamina_restore": 5,
"emoji": "\ud83e\udd6b",
"emoji": "🥫",
"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": {
"name": "Bottled Water",
"name": {
"en": "Bottled Water",
"es": "Agua embotellada"
},
"weight": 0.5,
"volume": 0.3,
"type": "consumable",
"stamina_restore": 10,
"emoji": "\ud83d\udca7",
"emoji": "💧",
"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": {
"name": "Water Bottle",
"name": {
"en": "Water Bottle",
"es": "Botella de agua"
},
"weight": 0.5,
"volume": 0.3,
"type": "consumable",
"stamina_restore": 10,
"emoji": "\ud83d\udca7",
"emoji": "💧",
"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": {
"name": "Energy Bar",
"name": {
"en": "Energy Bar",
"es": "Barra de energía"
},
"weight": 0.1,
"volume": 0.1,
"type": "consumable",
"stamina_restore": 15,
"emoji": "\ud83c\udf6b",
"emoji": "🍫",
"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": {
"name": "Mystery Pills",
"name": {
"en": "Mystery Pills",
"es": "Píldoras misteriosas"
},
"weight": 0.05,
"volume": 0.05,
"type": "consumable",
"hp_restore": 30,
"emoji": "\ud83d\udc8a",
"emoji": "💊",
"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": {
"name": "First Aid Kit",
"description": "A professional medical kit with bandages, antiseptic, and pain relievers.",
"name": {
"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,
"volume": 0.5,
"type": "consumable",
"hp_restore": 50,
"emoji": "\ud83e\ude79",
"emoji": "🩹",
"image_path": "images/items/first_aid_kit.webp"
},
"bandage": {
"name": "Bandage",
"description": "Clean cloth bandages for treating minor wounds. Can stop bleeding.",
"name": {
"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,
"volume": 0.1,
"type": "consumable",
"hp_restore": 15,
"treats": "Bleeding",
"emoji": "\ud83e\ude79",
"emoji": "🩹",
"image_path": "images/items/bandage.webp"
},
"medical_supplies": {
"name": "Medical Supplies",
"description": "Assorted medical supplies scavenged from a clinic.",
"name": {
"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,
"volume": 0.4,
"type": "consumable",
"hp_restore": 40,
"emoji": "\u2695\ufe0f",
"emoji": "⚕️",
"image_path": "images/items/medical_supplies.webp"
},
"antibiotics": {
"name": "Antibiotics",
"description": "Pills that fight infections. Expired, but better than nothing.",
"name": {
"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,
"volume": 0.1,
"type": "consumable",
"hp_restore": 20,
"treats": "Infected",
"emoji": "\ud83d\udc8a",
"emoji": "💊",
"image_path": "images/items/antibiotics.webp"
},
"rad_pills": {
"name": "Rad Pills",
"description": "Anti-radiation medication. Helps flush radioactive particles from the body.",
"name": {
"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,
"volume": 0.05,
"type": "consumable",
"hp_restore": 5,
"treats": "Radiation",
"emoji": "\u2622\ufe0f",
"emoji": "☢️",
"image_path": "images/items/rad_pills.webp"
},
"tire_iron": {
"name": "Tire Iron",
"description": "A heavy metal tool. Makes a decent improvised weapon.",
"name": {
"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,
"volume": 1.0,
"type": "weapon",
@@ -252,17 +396,23 @@
"damage_min": 3,
"damage_max": 5
},
"emoji": "\ud83d\udd27",
"emoji": "🔧",
"image_path": "images/items/tire_iron.webp"
},
"baseball_bat": {
"name": "Baseball Bat",
"description": "Wooden bat with dents and bloodstains. Someone used this before you.",
"name": {
"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,
"volume": 1.5,
"type": "weapon",
"slot": "hand",
"emoji": "\u26be",
"emoji": "",
"image_path": "images/items/baseball_bat.webp",
"stats": {
"damage_min": 5,
@@ -270,8 +420,14 @@
}
},
"rusty_knife": {
"name": "Rusty Knife",
"description": "A dull, rusted blade. Better than your fists.",
"name": {
"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,
"volume": 0.2,
"type": "weapon",
@@ -296,12 +452,18 @@
"damage_min": 2,
"damage_max": 5
},
"emoji": "\ud83d\udd2a",
"emoji": "🔪",
"image_path": "images/items/rusty_knife.webp"
},
"knife": {
"name": "Knife",
"description": "A sharp survival knife in decent condition.",
"name": {
"en": "Knife",
"es": ""
},
"description": {
"en": "A sharp survival knife in decent condition.",
"es": ""
},
"weight": 0.3,
"volume": 0.2,
"type": "weapon",
@@ -379,17 +541,23 @@
"duration": 3
}
},
"emoji": "\ud83d\udd2a",
"emoji": "🔪",
"image_path": "images/items/knife.webp"
},
"rusty_pipe": {
"name": "Rusty Pipe",
"description": "Heavy metal pipe. Crude but effective.",
"name": {
"en": "Rusty Pipe",
"es": ""
},
"description": {
"en": "Heavy metal pipe. Crude but effective.",
"es": ""
},
"weight": 1.5,
"volume": 0.8,
"type": "weapon",
"slot": "hand",
"emoji": "\ud83d\udd29",
"emoji": "🔩",
"image_path": "images/items/rusty_pipe.webp",
"stats": {
"damage_min": 5,
@@ -397,8 +565,14 @@
}
},
"tattered_rucksack": {
"name": "Tattered Rucksack",
"description": "An old backpack with torn straps. Still functional.",
"name": {
"en": "Tattered Rucksack",
"es": ""
},
"description": {
"en": "An old backpack with torn straps. Still functional.",
"es": ""
},
"weight": 1.0,
"volume": 0.5,
"type": "backpack",
@@ -434,12 +608,18 @@
"weight_capacity": 10,
"volume_capacity": 10
},
"emoji": "\ud83c\udf92",
"emoji": "🎒",
"image_path": "images/items/tattered_rucksack.webp"
},
"hiking_backpack": {
"name": "Hiking Backpack",
"description": "A quality backpack with multiple compartments.",
"name": {
"en": "Hiking Backpack",
"es": ""
},
"description": {
"en": "A quality backpack with multiple compartments.",
"es": ""
},
"weight": 1.5,
"volume": 0.7,
"type": "backpack",
@@ -464,17 +644,23 @@
"weight_capacity": 20,
"volume_capacity": 20
},
"emoji": "\ud83c\udf92",
"emoji": "🎒",
"image_path": "images/items/hiking_backpack.webp"
},
"flashlight": {
"name": "Flashlight",
"description": "A battery-powered flashlight. Batteries low but working.",
"name": {
"en": "Flashlight",
"es": ""
},
"description": {
"en": "A battery-powered flashlight. Batteries low but working.",
"es": ""
},
"weight": 0.3,
"volume": 0.2,
"type": "tool",
"slot": "tool",
"emoji": "\ud83d\udd26",
"emoji": "🔦",
"image_path": "images/items/flashlight.webp",
"stats": {
"damage_min": 5,
@@ -482,26 +668,44 @@
}
},
"old_photograph": {
"name": "Old Photograph",
"name": {
"en": "Old Photograph",
"es": ""
},
"weight": 0.01,
"volume": 0.01,
"type": "quest",
"emoji": "\ud83d\udcf7",
"emoji": "📷",
"image_path": "images/items/old_photograph.webp",
"description": "A useful old photograph."
"description": {
"en": "A useful old photograph.",
"es": ""
}
},
"key_ring": {
"name": "Key Ring",
"name": {
"en": "Key Ring",
"es": ""
},
"weight": 0.1,
"volume": 0.05,
"type": "quest",
"emoji": "\ud83d\udd11",
"emoji": "🔑",
"image_path": "images/items/key_ring.webp",
"description": "A useful key ring."
"description": {
"en": "A useful key ring.",
"es": ""
}
},
"makeshift_spear": {
"name": "Makeshift Spear",
"description": "A crude spear made from a sharpened stick and scrap metal.",
"name": {
"en": "Makeshift Spear",
"es": ""
},
"description": {
"en": "A crude spear made from a sharpened stick and scrap metal.",
"es": ""
},
"weight": 1.2,
"volume": 2.0,
"type": "weapon",
@@ -541,12 +745,18 @@
"damage_min": 4,
"damage_max": 7
},
"emoji": "\u2694\ufe0f",
"emoji": "⚔️",
"image_path": "images/items/makeshift_spear.webp"
},
"reinforced_bat": {
"name": "Reinforced Bat",
"description": "A wooden bat wrapped with scrap metal and nails. Brutal.",
"name": {
"en": "Reinforced Bat",
"es": ""
},
"description": {
"en": "A wooden bat wrapped with scrap metal and nails. Brutal.",
"es": ""
},
"weight": 1.8,
"volume": 1.5,
"type": "weapon",
@@ -592,12 +802,18 @@
"duration": 1
}
},
"emoji": "\ud83c\udff8",
"emoji": "🏸",
"image_path": "images/items/reinforced_bat.webp"
},
"leather_vest": {
"name": "Leather Vest",
"description": "A makeshift vest crafted from leather scraps. Provides basic protection.",
"name": {
"en": "Leather Vest",
"es": ""
},
"description": {
"en": "A makeshift vest crafted from leather scraps. Provides basic protection.",
"es": ""
},
"weight": 1.5,
"volume": 1.0,
"type": "armor",
@@ -637,12 +853,18 @@
"armor": 3,
"hp_bonus": 10
},
"emoji": "\ud83e\uddba",
"emoji": "🦺",
"image_path": "images/items/leather_vest.webp"
},
"cloth_bandana": {
"name": "Cloth Bandana",
"description": "A simple cloth head covering. Keeps the sun and dust out.",
"name": {
"en": "Cloth Bandana",
"es": ""
},
"description": {
"en": "A simple cloth head covering. Keeps the sun and dust out.",
"es": ""
},
"weight": 0.1,
"volume": 0.1,
"type": "clothing",
@@ -669,12 +891,18 @@
"stats": {
"armor": 1
},
"emoji": "\ud83e\udde3",
"emoji": "🧣",
"image_path": "images/items/cloth_bandana.webp"
},
"sturdy_boots": {
"name": "Sturdy Boots",
"description": "Reinforced boots for traversing the wasteland.",
"name": {
"en": "Sturdy Boots",
"es": ""
},
"description": {
"en": "Reinforced boots for traversing the wasteland.",
"es": ""
},
"weight": 1.0,
"volume": 0.8,
"type": "clothing",
@@ -714,12 +942,18 @@
"armor": 2,
"stamina_bonus": 5
},
"emoji": "\ud83e\udd7e",
"emoji": "🥾",
"image_path": "images/items/sturdy_boots.webp"
},
"padded_pants": {
"name": "Padded Pants",
"description": "Pants reinforced with extra padding for protection.",
"name": {
"en": "Padded Pants",
"es": ""
},
"description": {
"en": "Pants reinforced with extra padding for protection.",
"es": ""
},
"weight": 0.8,
"volume": 0.6,
"type": "armor",
@@ -755,12 +989,18 @@
"armor": 2,
"hp_bonus": 5
},
"emoji": "\ud83d\udc56",
"emoji": "👖",
"image_path": "images/items/padded_pants.webp"
},
"reinforced_pack": {
"name": "Reinforced Pack",
"description": "A custom-built backpack with metal frame and extra pockets.",
"name": {
"en": "Reinforced Pack",
"es": ""
},
"description": {
"en": "A custom-built backpack with metal frame and extra pockets.",
"es": ""
},
"weight": 2.0,
"volume": 0.9,
"type": "backpack",
@@ -839,12 +1079,18 @@
"weight_capacity": 30,
"volume_capacity": 30
},
"emoji": "\ud83c\udf92",
"emoji": "🎒",
"image_path": "images/items/reinforced_pack.webp"
},
"hammer": {
"name": "Hammer",
"description": "A basic tool for crafting and repairs. Essential for any survivor.",
"name": {
"en": "Hammer",
"es": ""
},
"description": {
"en": "A basic tool for crafting and repairs. Essential for any survivor.",
"es": ""
},
"weight": 0.8,
"volume": 0.4,
"type": "tool",
@@ -872,12 +1118,18 @@
}
],
"repair_percentage": 30,
"emoji": "\ud83d\udd28",
"emoji": "🔨",
"image_path": "images/items/hammer.webp"
},
"screwdriver": {
"name": "Screwdriver",
"description": "A flathead screwdriver. Useful for repairs and scavenging.",
"name": {
"en": "Screwdriver",
"es": ""
},
"description": {
"en": "A flathead screwdriver. Useful for repairs and scavenging.",
"es": ""
},
"weight": 0.2,
"volume": 0.2,
"type": "tool",
@@ -905,7 +1157,7 @@
}
],
"repair_percentage": 25,
"emoji": "\ud83e\ude9b",
"emoji": "🪛",
"image_path": "images/items/screwdriver.webp",
"stats": {
"damage_min": 5,

View File

@@ -2,8 +2,14 @@
"locations": [
{
"id": "start_point",
"name": "\ud83c\udf06 Ruined Downtown Core",
"description": "The wind howls through skeletal skyscrapers. Debris litters the cracked asphalt. You sense danger, but also opportunity.",
"name": {
"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",
"x": 0,
"y": 0,
@@ -33,10 +39,19 @@
"stamina_cost": 2,
"success_rate": 0.5,
"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": "",
"failure": "Just rotting garbage. Nothing useful.",
"success": "Despite the smell, you find some [Plastic Bottles] and [Cloth Scraps]."
"failure": {
"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": {
"crit_failure": "",
"crit_success": "",
"failure": "The trunk is rusted shut. You can't get it open.",
"success": "With a great heave, you pry the trunk open and find a [Tire Iron]!"
"failure": {
"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": {
@@ -88,8 +109,14 @@
"text": {
"crit_failure": "",
"crit_success": "",
"failure": "The glovebox is empty except for dust and old receipts.",
"success": "You find a half-eaten [Stale Chocolate Bar]."
"failure": {
"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",
"name": "\u26fd\ufe0f Abandoned Gas Station",
"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.",
"name": {
"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",
"x": 0,
"y": 2,
@@ -141,10 +174,22 @@
]
},
"text": {
"success": "You find some cloth scraps and plastic in the glovebox.",
"failure": "The glovebox is empty except for old papers.",
"crit_success": "You find scrap metal from the dashboard!",
"crit_failure": "The glovebox is jammed shut."
"success": {
"en": "You find some cloth scraps and plastic in the glovebox.",
"es": ""
},
"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": {
@@ -176,10 +221,22 @@
]
},
"text": {
"success": "You force the trunk open and find scrap metal and plastic.",
"failure": "The trunk is rusted shut.",
"crit_success": "The trunk contains tools!",
"crit_failure": "You cut your hand on rusty metal! (-5 HP)"
"success": {
"en": "You force the trunk open and find scrap metal and plastic.",
"es": ""
},
"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": {
"success": "You find scrap metal and cloth in the storage box.",
"failure": "The storage box is mostly empty.",
"crit_success": "You discover tools inside!",
"crit_failure": "Just oil stains and rust."
"success": {
"en": "You find scrap metal and cloth in the storage box.",
"es": ""
},
"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",
"name": "\ud83c\udfd8\ufe0f Residential Street",
"description": "A quiet suburban street lined with abandoned homes. Most are boarded up, but a few doors hang open, creaking in the wind.",
"name": {
"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",
"x": 3,
"y": 0,
@@ -264,10 +339,19 @@
"stamina_cost": 3,
"success_rate": 0.5,
"text": {
"crit_failure": "The floor collapses beneath you! (-10 HP)",
"crit_failure": {
"en": "The floor collapses beneath you! (-10 HP)",
"es": ""
},
"crit_success": "",
"failure": "The house has already been thoroughly looted. Nothing remains.",
"success": "You find some useful supplies: [Canned Beans], [Bottled Water], and [Cloth Scraps]!"
"failure": {
"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",
"name": "\ud83c\udfe5 Old Clinic",
"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.",
"name": {
"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",
"x": 2,
"y": 3,
@@ -310,8 +400,14 @@
"text": {
"crit_failure": "",
"crit_success": "",
"failure": "The cabinet is empty. Someone got here first.",
"success": "Jackpot! You find a [First Aid Kit] and some [Bandages]!"
"failure": {
"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",
"name": "\ud83c\udfec Shopping Plaza",
"description": "A strip mall with broken storefronts. Most shops have been thoroughly ransacked, but you might find something if you search carefully.",
"name": {
"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",
"x": -2.5,
"y": 0,
@@ -359,10 +461,22 @@
]
},
"text": {
"success": "You smash the vending machine and grab bottles and scrap.",
"failure": "The machine is too sturdy to break.",
"crit_success": "Packaged food falls out!",
"crit_failure": "Glass cuts your arm! (-10 HP)"
"success": {
"en": "You smash the vending machine and grab bottles and scrap.",
"es": ""
},
"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": {
@@ -389,10 +503,22 @@
]
},
"text": {
"success": "You find a plastic bottle at the bottom.",
"failure": "Nothing left to scavenge.",
"crit_success": "A snack is wedged in the dispenser!",
"crit_failure": "Already picked clean."
"success": {
"en": "You find a plastic bottle at the bottom.",
"es": ""
},
"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": {
"success": "You dig through rubble and find scrap metal and cloth.",
"failure": "Just broken concrete and dust.",
"crit_success": "A tool was buried in the debris!",
"crit_failure": "Sharp debris cuts you! (-5 HP)"
"success": {
"en": "You dig through rubble and find scrap metal and cloth.",
"es": ""
},
"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",
"name": "\ud83c\udf33 Suburban Park",
"description": "An overgrown park with rusted playground equipment. Nature is slowly reclaiming this space. A maintenance shed sits at the far end.",
"name": {
"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",
"x": -1,
"y": -2,
@@ -484,8 +628,14 @@
"text": {
"crit_failure": "",
"crit_success": "",
"failure": "The shed has been picked clean. Only empty shelves remain.",
"success": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!"
"failure": {
"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",
"name": "\ud83d\udee3\ufe0f Highway Overpass",
"description": "A concrete overpass spanning the cracked highway below. Abandoned vehicles litter the road. This is a good vantage point to survey the area.",
"name": {
"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,
"y": 4.5,
"image_path": "images/locations/overpass.webp",
@@ -510,8 +666,14 @@
"crit_success_chance": 0.1,
"crit_failure_chance": 0.1,
"text": {
"success": "You find a half-eaten [Stale Chocolate Bar].",
"failure": "The glovebox is empty except for dust and old receipts.",
"success": {
"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_failure": ""
},
@@ -534,8 +696,14 @@
"crit_success_chance": 0.1,
"crit_failure_chance": 0.1,
"text": {
"success": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
"failure": "The trunk is rusted shut. You can't get it open.",
"success": {
"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_failure": ""
},
@@ -563,8 +731,14 @@
"crit_success_chance": 0.1,
"crit_failure_chance": 0.1,
"text": {
"success": "You find a half-eaten [Stale Chocolate Bar].",
"failure": "The glovebox is empty except for dust and old receipts.",
"success": {
"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_failure": ""
},
@@ -587,8 +761,14 @@
"crit_success_chance": 0.1,
"crit_failure_chance": 0.1,
"text": {
"success": "With a great heave, you pry the trunk open and find a [Tire Iron]!",
"failure": "The trunk is rusted shut. You can't get it open.",
"success": {
"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_failure": ""
},
@@ -611,8 +791,14 @@
},
{
"id": "warehouse",
"name": "\ud83c\udfed Warehouse District",
"description": "Rows of industrial warehouses stretch before you. Metal doors creak in the wind. The loading docks are littered with debris and abandoned cargo.",
"name": {
"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",
"x": 4,
"y": -1.5,
@@ -642,10 +828,19 @@
"stamina_cost": 2,
"success_rate": 0.5,
"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": "",
"failure": "Just rotting garbage. Nothing useful.",
"success": "Despite the smell, you find some [Plastic Bottles] and [Cloth Scraps]."
"failure": {
"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": {
"crit_failure": "",
"crit_success": "",
"failure": "The shed has been picked clean. Only empty shelves remain.",
"success": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!"
"failure": {
"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",
"name": "\ud83d\udce6 Warehouse Interior",
"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.",
"name": {
"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",
"x": 4.5,
"y": -2,
@@ -709,8 +916,14 @@
"crit_success_chance": 0,
"crit_failure_chance": 0,
"text": {
"success": "You successfully \ud83d\udd0e search box.",
"failure": "You failed to \ud83d\udd0e search box.",
"success": {
"en": "You successfully 🔎 search box.",
"es": ""
},
"failure": {
"en": "You failed to 🔎 search box.",
"es": ""
},
"crit_success": "",
"crit_failure": ""
},
@@ -738,8 +951,14 @@
},
{
"id": "subway",
"name": "\ud83d\ude87 Subway Station Entrance",
"description": "Stairs descend into darkness. The entrance to an abandoned subway station yawns before you. Emergency lighting flickers somewhere below.",
"name": {
"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",
"x": -4,
"y": -0.5,
@@ -775,10 +994,22 @@
"stamina_cost": 2,
"success_rate": 0.55,
"text": {
"crit_failure": "Debris shifts and hits your leg! (-4 HP)",
"crit_success": "You uncover a tool buried deep!",
"failure": "Just concrete chunks.",
"success": "You sift through rubble and find scrap metal."
"crit_failure": {
"en": "Debris shifts and hits your leg! (-4 HP)",
"es": ""
},
"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,
"success_rate": 0.6,
"text": {
"crit_failure": "The machine topples on you! (-12 HP)",
"crit_success": "Food packages tumble out!",
"failure": "The machine won't budge.",
"success": "You bash open the vending machine and grab bottles."
"crit_failure": {
"en": "The machine topples on you! (-12 HP)",
"es": ""
},
"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": {
@@ -834,10 +1077,22 @@
"stamina_cost": 2,
"success_rate": 0.4,
"text": {
"crit_failure": "Nothing here.",
"crit_success": "A bottle still rolls out!",
"failure": "Completely empty.",
"success": "You find a bottle in the machine's slot."
"crit_failure": {
"en": "Nothing here.",
"es": ""
},
"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",
"name": "\ud83d\ude8a Subway Tunnels",
"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.",
"name": {
"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",
"x": -4.5,
"y": -1,
@@ -880,10 +1141,22 @@
]
},
"text": {
"success": "You find scrap metal in the tunnel debris.",
"failure": "Just rocks and dirt.",
"crit_success": "A maintenance tool was left behind!",
"crit_failure": "You stumble and hit the wall! (-6 HP)"
"success": {
"en": "You find scrap metal in the tunnel debris.",
"es": ""
},
"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",
"name": "\ud83c\udfe2 Office Building",
"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.",
"name": {
"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",
"x": 3.5,
"y": 4,
@@ -924,10 +1203,22 @@
"crit_items": []
},
"text": {
"success": "You find scrap metal and cloth in the lobby debris.",
"failure": "Just broken furniture and papers.",
"crit_success": "You discover useful materials!",
"crit_failure": "Glass cuts your hand! (-5 HP)"
"success": {
"en": "You find scrap metal and cloth in the lobby debris.",
"es": ""
},
"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",
"name": "\ud83d\udcbc Office Floors",
"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.",
"name": {
"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",
"x": 4,
"y": 4.5,
@@ -974,10 +1271,22 @@
]
},
"text": {
"success": "You find cloth and bottles in desk drawers.",
"failure": "Everything's been picked through already.",
"crit_success": "Someone left food in their desk!",
"crit_failure": "Just old paperwork."
"success": {
"en": "You find cloth and bottles in desk drawers.",
"es": ""
},
"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",
"name": "Subway Section A",
"description": "A shady dimly lit subway section. All you can see are abandoned train tracks and some garbage lying around. ",
"name": {
"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",
"x": -5,
"y": -2,
@@ -1019,10 +1334,22 @@
]
},
"text": {
"success": "You dig through the garbage and find scrap metal.",
"failure": "Just rotting trash.",
"crit_success": "A tool was discarded here!",
"crit_failure": "You step on sharp debris! (-5 HP)"
"success": {
"en": "You dig through the garbage and find scrap metal.",
"es": ""
},
"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": ""
}
}
}
}

View File

@@ -2,8 +2,14 @@
"npcs": {
"feral_dog": {
"npc_id": "feral_dog",
"name": "Feral Dog",
"description": "A wild, mangy dog with desperate hunger in its eyes. Its ribs are visible beneath matted fur.",
"name": {
"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": "🐕",
"hp_min": 15,
"hp_max": 25,
@@ -46,8 +52,14 @@
},
"raider_scout": {
"npc_id": "raider_scout",
"name": "Raider Scout",
"description": "A lone raider wearing makeshift armor. They eye you with hostile intent.",
"name": {
"en": "Raider Scout",
"es": ""
},
"description": {
"en": "A lone raider wearing makeshift armor. They eye you with hostile intent.",
"es": ""
},
"emoji": "🏴‍☠️",
"hp_min": 30,
"hp_max": 45,
@@ -102,8 +114,14 @@
},
"mutant_rat": {
"npc_id": "mutant_rat",
"name": "Mutant Rat",
"description": "A grotesquely large rat, its fur patchy and eyes glowing with unnatural light.",
"name": {
"en": "Mutant Rat",
"es": ""
},
"description": {
"en": "A grotesquely large rat, its fur patchy and eyes glowing with unnatural light.",
"es": ""
},
"emoji": "🐀",
"hp_min": 10,
"hp_max": 18,
@@ -140,8 +158,14 @@
},
"infected_human": {
"npc_id": "infected_human",
"name": "Infected Human",
"description": "Once human, now something else. Their movements are jerky and their skin shows signs of advanced infection.",
"name": {
"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": "🧟",
"hp_min": 35,
"hp_max": 50,
@@ -184,8 +208,14 @@
},
"scavenger": {
"npc_id": "scavenger",
"name": "Hostile Scavenger",
"description": "Another survivor, but this one sees you as competition. They won't share territory.",
"name": {
"en": "Hostile Scavenger",
"es": ""
},
"description": {
"en": "Another survivor, but this one sees you as competition. They won't share territory.",
"es": ""
},
"emoji": "💀",
"hp_min": 25,
"hp_max": 40,
@@ -264,23 +294,23 @@
},
"residential": {
"danger_level": 1,
"encounter_rate": 0.10,
"wandering_chance": 0.20
"encounter_rate": 0.1,
"wandering_chance": 0.2
},
"park": {
"danger_level": 1,
"encounter_rate": 0.10,
"wandering_chance": 0.20
"encounter_rate": 0.1,
"wandering_chance": 0.2
},
"clinic": {
"danger_level": 2,
"encounter_rate": 0.20,
"encounter_rate": 0.2,
"wandering_chance": 0.35
},
"plaza": {
"danger_level": 2,
"encounter_rate": 0.15,
"wandering_chance": 0.30
"wandering_chance": 0.3
},
"warehouse": {
"danger_level": 2,
@@ -290,27 +320,27 @@
"warehouse_interior": {
"danger_level": 2,
"encounter_rate": 0.22,
"wandering_chance": 0.40
"wandering_chance": 0.4
},
"overpass": {
"danger_level": 3,
"encounter_rate": 0.30,
"encounter_rate": 0.3,
"wandering_chance": 0.45
},
"office_building": {
"danger_level": 3,
"encounter_rate": 0.25,
"wandering_chance": 0.40
"wandering_chance": 0.4
},
"office_interior": {
"danger_level": 3,
"encounter_rate": 0.35,
"wandering_chance": 0.50
"wandering_chance": 0.5
},
"subway": {
"danger_level": 4,
"encounter_rate": 0.35,
"wandering_chance": 0.50
"wandering_chance": 0.5
},
"subway_tunnels": {
"danger_level": 4,

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 678 KiB

View File

Before

Width:  |  Height:  |  Size: 881 KiB

After

Width:  |  Height:  |  Size: 881 KiB

View File

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 602 KiB

View File

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 528 KiB

View File

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

View File

Before

Width:  |  Height:  |  Size: 859 KiB

After

Width:  |  Height:  |  Size: 859 KiB

View File

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

View File

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

View File

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 758 KiB

View File

Before

Width:  |  Height:  |  Size: 552 KiB

After

Width:  |  Height:  |  Size: 552 KiB

View File

Before

Width:  |  Height:  |  Size: 677 KiB

After

Width:  |  Height:  |  Size: 677 KiB

View File

Before

Width:  |  Height:  |  Size: 804 KiB

After

Width:  |  Height:  |  Size: 804 KiB

View File

Before

Width:  |  Height:  |  Size: 507 KiB

After

Width:  |  Height:  |  Size: 507 KiB

View File

Before

Width:  |  Height:  |  Size: 538 KiB

After

Width:  |  Height:  |  Size: 538 KiB

View File

Before

Width:  |  Height:  |  Size: 947 KiB

After

Width:  |  Height:  |  Size: 947 KiB

View File

Before

Width:  |  Height:  |  Size: 698 KiB

After

Width:  |  Height:  |  Size: 698 KiB

View File

Before

Width:  |  Height:  |  Size: 822 KiB

After

Width:  |  Height:  |  Size: 822 KiB

View File

Before

Width:  |  Height:  |  Size: 535 KiB

After

Width:  |  Height:  |  Size: 535 KiB

View File

Before

Width:  |  Height:  |  Size: 961 KiB

After

Width:  |  Height:  |  Size: 961 KiB

View File

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

View File

Before

Width:  |  Height:  |  Size: 905 KiB

After

Width:  |  Height:  |  Size: 905 KiB

View File

Before

Width:  |  Height:  |  Size: 980 KiB

After

Width:  |  Height:  |  Size: 980 KiB

View File

Before

Width:  |  Height:  |  Size: 803 KiB

After

Width:  |  Height:  |  Size: 803 KiB

View File

Before

Width:  |  Height:  |  Size: 864 KiB

After

Width:  |  Height:  |  Size: 864 KiB

View File

Before

Width:  |  Height:  |  Size: 694 KiB

After

Width:  |  Height:  |  Size: 694 KiB

View File

Before

Width:  |  Height:  |  Size: 777 KiB

After

Width:  |  Height:  |  Size: 777 KiB

View File

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 696 KiB

View File

Before

Width:  |  Height:  |  Size: 636 KiB

After

Width:  |  Height:  |  Size: 636 KiB

View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

View File

Before

Width:  |  Height:  |  Size: 860 KiB

After

Width:  |  Height:  |  Size: 860 KiB

View File

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 648 KiB

View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 338 KiB

View File

Before

Width:  |  Height:  |  Size: 898 KiB

After

Width:  |  Height:  |  Size: 898 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 933 KiB

After

Width:  |  Height:  |  Size: 933 KiB

View File

Before

Width:  |  Height:  |  Size: 460 KiB

After

Width:  |  Height:  |  Size: 460 KiB

View File

Before

Width:  |  Height:  |  Size: 749 KiB

After

Width:  |  Height:  |  Size: 749 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

57
images-source/make_webp.sh Executable file
View 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"

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -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
View File

@@ -0,0 +1,188 @@
# Echoes of the Ash
> A post-apocalyptic survival RPG - Browser-based MUD-style game
![Status](https://img.shields.io/badge/Status-In%20Development-yellow)
![Platform](https://img.shields.io/badge/Platform-Web%20%7C%20PWA%20%7C%20Electron%20%7C%20Steam-blue)
![License](https://img.shields.io/badge/License-Proprietary-red)
## 🎮 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
View File

@@ -6,6 +6,10 @@ yarn.lock
# Build output
dist/
build/
dist-electron/
# Copied assets (generated at build time)
public/images/
# Environment variables
.env

View 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...')
}
}

View File

@@ -40,7 +40,7 @@ function createWindow() {
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
preload: path.join(__dirname, 'preload.cjs')
},
icon: path.join(__dirname, 'icons/icon.png'),
title: 'Echoes of the Ash'

View File

@@ -9,17 +9,18 @@
},
"homepage": "https://echoesoftheash.com",
"type": "module",
"main": "electron/main.js",
"main": "electron/main.cjs",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"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:build": "npm run build && electron-builder",
"electron:build:win": "npm run build && electron-builder --win",
"electron:build:linux": "npm run build && electron-builder --linux",
"electron:build:mac": "npm run build && electron-builder --mac"
"electron:build": "npm run copy-assets && npm run build && electron-builder",
"electron:build:win": "npm run copy-assets && npm run build && electron-builder --win",
"electron:build:linux": "npm run copy-assets && npm run build && electron-builder --linux",
"electron:build:mac": "npm run copy-assets && npm run build && electron-builder --mac"
},
"dependencies": {
"react": "^18.2.0",
@@ -27,7 +28,10 @@
"react-router-dom": "^6.20.0",
"axios": "^1.6.2",
"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": {
"@types/react": "^18.2.43",
@@ -52,6 +56,7 @@
"build": {
"appId": "com.echoesoftheash.game",
"productName": "Echoes of the Ash",
"afterPack": "./electron/afterPack.cjs",
"directories": {
"output": "dist-electron"
},

View File

@@ -3,6 +3,8 @@ import { useNavigate, useLocation } from 'react-router-dom'
import { useAuth } from '../hooks/useAuth'
import { useGameWebSocket } from '../hooks/useGameWebSocket'
import api from '../services/api'
import { useTranslation } from 'react-i18next'
import LanguageSelector from './LanguageSelector'
import './Game.css'
interface GameHeaderProps {
@@ -13,6 +15,7 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
const navigate = useNavigate()
const location = useLocation()
const { currentCharacter, logout } = useAuth()
const { t } = useTranslation()
const [playerCount, setPlayerCount] = useState<number>(0)
// Fetch initial player count
@@ -63,19 +66,20 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
onClick={() => navigate('/game')}
className={`nav-link ${isActive('/game') ? 'active' : ''}`}
>
🎮 Game
🎮 {t('common.game')}
</button>
<button
onClick={() => navigate('/leaderboards')}
className={`nav-link ${isActive('/leaderboards') ? 'active' : ''}`}
>
🏆 Leaderboards
🏆 {t('common.leaderboards')}
</button>
</nav>
<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="count-text">{playerCount} Online</span>
<span className="count-text">{t('game.onlineCount', { count: playerCount })}</span>
</div>
<button
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
@@ -87,9 +91,9 @@ export default function GameHeader({ className = '' }: GameHeaderProps) {
onClick={() => navigate('/account')}
className="button-secondary"
>
Account
{t('common.account')}
</button>
<button onClick={logout} className="button-secondary">Logout</button>
<button onClick={logout} className="button-secondary">{t('auth.logout')}</button>
</div>
</header>
)

View 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;
}

View 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

View File

@@ -1,5 +1,8 @@
import { MouseEvent, ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { PlayerState, Profile, Equipment } from './types'
import { getAssetPath } from '../../utils/assetPath'
import { getTranslatedText } from '../../utils/i18nUtils'
import './InventoryModal.css'
interface InventoryModalProps {
@@ -31,6 +34,7 @@ function InventoryModal({
onUnequipItem,
onDropItem
}: InventoryModalProps) {
useTranslation()
// Categories for the sidebar
const categories = [
{ id: 'all', label: 'All Items', icon: '🎒' },
@@ -51,7 +55,7 @@ function InventoryModal({
// Filter items based on search and category
const filteredItems = allItems
.filter((item: any) => {
const itemName = item.name || 'Unknown Item';
const itemName = getTranslatedText(item.name) || 'Unknown Item';
const matchesSearch = itemName.toLowerCase().includes(inventoryFilter.toLowerCase())
const matchesCategory = inventoryCategoryFilter === 'all' || item.type === inventoryCategoryFilter
return matchesSearch && matchesCategory
@@ -60,7 +64,7 @@ function InventoryModal({
// Equipped items first
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) => {
@@ -75,8 +79,8 @@ function InventoryModal({
<div className="item-image-section small">
{item.image_path ? (
<img
src={item.image_path}
alt={item.name}
src={getAssetPath(item.image_path)}
alt={getTranslatedText(item.name)}
className="item-img-thumb"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
@@ -95,7 +99,7 @@ function InventoryModal({
<div className="item-info-section">
<div className="item-header-compact">
<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>}
</div>
@@ -116,7 +120,7 @@ function InventoryModal({
{/* Stats & Durability */}
<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 */}
<div className="stat-badges-container">
@@ -315,7 +319,7 @@ function InventoryModal({
{equipment?.backpack ? (
<div className="backpack-status active">
<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">
(+{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)

View File

@@ -1,6 +1,9 @@
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
import { useTranslation } from 'react-i18next'
import Workbench from './Workbench'
import { getAssetPath } from '../../utils/assetPath'
import { getTranslatedText } from '../../utils/i18nUtils'
interface LocationViewProps {
location: Location
@@ -79,11 +82,12 @@ function LocationView({
onRepair,
onUncraft
}: LocationViewProps) {
useTranslation()
return (
<div className="location-view">
<div className="location-info">
<h2 className="centered-heading">
{location.name}
{getTranslatedText(location.name)}
{location.danger_level !== undefined && location.danger_level === 0 && (
<span className="danger-badge danger-safe" title="Safe Zone"> Safe</span>
)}
@@ -132,8 +136,8 @@ function LocationView({
{location.image_url && (
<div className="location-image-container">
<img
src={location.image_url}
alt={location.name}
src={getAssetPath(location.image_url)}
alt={getTranslatedText(location.name)}
className="location-image"
onError={(e: any) => (e.currentTarget.style.display = 'none')}
/>
@@ -141,7 +145,7 @@ function LocationView({
)}
<div className="location-description-box">
<p className="location-description">{location.description}</p>
<p className="location-description">{getTranslatedText(location.description)}</p>
</div>
</div>
@@ -176,7 +180,7 @@ function LocationView({
{enemy.id && (
<div className="entity-image">
<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}
onError={(e: any) => { e.currentTarget.style.display = 'none' }}
/>
@@ -301,7 +305,7 @@ function LocationView({
<div key={i} className="entity-card item-card">
{item.image_path ? (
<img
src={item.image_path}
src={getAssetPath(item.image_path)}
alt={item.name}
className="entity-icon"
onError={(e) => {

View File

@@ -1,5 +1,7 @@
import type { Location, Profile, CombatState } from './types'
import { useState, useEffect } from 'react'
import { getAssetPath } from '../../utils/assetPath'
import { getTranslatedText } from '../../utils/i18nUtils'
interface MovementControlsProps {
location: Location
@@ -45,7 +47,7 @@ function MovementControls({
// Helper function to get destination name for a direction
const getDestinationName = (direction: string): string => {
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
@@ -197,8 +199,8 @@ function MovementControls({
{interactable.image_path && (
<div className="interactable-image-container">
<img
src={`/${interactable.image_path}`}
alt={interactable.name}
src={getAssetPath(interactable.image_path)}
alt={getTranslatedText(interactable.name)}
className="interactable-image"
onError={(e: any) => {
e.currentTarget.style.display = 'none'
@@ -208,7 +210,7 @@ function MovementControls({
)}
<div className="interactable-content">
<div className="interactable-header">
<span className="interactable-name">{interactable.name}</span>
<span className="interactable-name">{getTranslatedText(interactable.name)}</span>
</div>
{interactable.actions && interactable.actions.length > 0 && (
<div className="interactable-actions">
@@ -231,10 +233,10 @@ function MovementControls({
? 'Cannot interact during combat'
: cooldownRemaining > 0
? `Wait ${cooldownRemaining}s`
: action.description
: getTranslatedText(action.description)
}
>
{action.name}
{getTranslatedText(action.name)}
<span className="stamina-cost">
{cooldownRemaining > 0 ? `${cooldownRemaining}s` : `${action.stamina_cost}`}
</span>

View File

@@ -1,5 +1,8 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { PlayerState, Profile, Equipment } from './types'
import { getAssetPath } from '../../utils/assetPath'
import { getTranslatedText } from '../../utils/i18nUtils'
import InventoryModal from './InventoryModal'
interface PlayerSidebarProps {
@@ -37,16 +40,18 @@ function PlayerSidebar({
const { t } = useTranslation()
const renderEquipmentSlot = (slot: string, item: any, emoji: string, label: string) => (
<div className={`equipment-slot ${item ? 'filled' : 'empty'}`}>
{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">
{item.image_path ? (
<img
src={item.image_path}
alt={item.name}
src={getAssetPath(item.image_path)}
alt={getTranslatedText(item.name)}
className="equipment-emoji"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
@@ -56,52 +61,52 @@ function PlayerSidebar({
/>
) : null}
<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 && (
<span className="equipment-durability">{item.durability}/{item.max_durability}</span>
)}
</div>
<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 */}
{(item.unique_stats || item.stats) && Object.keys(item.unique_stats || item.stats).length > 0 && (
<>
{(item.unique_stats?.armor || item.stats?.armor) && (
<div className="item-tooltip-stat">
🛡 Armor: +{item.unique_stats?.armor || item.stats?.armor}
{t('stats.armor')}: +{item.unique_stats?.armor || item.stats?.armor}
</div>
)}
{(item.unique_stats?.hp_max || item.stats?.hp_max) && (
<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>
)}
{(item.unique_stats?.stamina_max || item.stats?.stamina_max) && (
<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>
)}
{(item.unique_stats?.damage_min !== undefined || item.stats?.damage_min !== undefined) &&
(item.unique_stats?.damage_max !== undefined || item.stats?.damage_max !== undefined) && (
<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>
)}
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
<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>
)}
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
<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>
)}
</>
)}
{item.durability !== undefined && item.durability !== null && (
<div className="item-tooltip-stat">
🔧 Durability: {item.durability}/{item.max_durability}
{t('stats.durability')}: {item.durability}/{item.max_durability}
</div>
)}
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
@@ -126,12 +131,12 @@ function PlayerSidebar({
<div className={`right-sidebar mobile-menu-panel ${mobileMenuOpen === 'right' ? 'open' : ''}`}>
{/* Profile Stats */}
<div className="profile-sidebar">
<h3>👤 Character</h3>
<h3>{t('game.character')}</h3>
<div className="sidebar-stat-bars">
<div className="sidebar-stat-bar">
<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>
</div>
<div className="sidebar-progress-bar">
@@ -145,7 +150,7 @@ function PlayerSidebar({
<div className="sidebar-stat-bar">
<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>
</div>
<div className="sidebar-progress-bar">
@@ -161,13 +166,13 @@ function PlayerSidebar({
{profile && (
<div className="sidebar-stats">
<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>
</div>
<div className="sidebar-stat-bar">
<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>
</div>
<div className="sidebar-progress-bar">
@@ -181,7 +186,7 @@ function PlayerSidebar({
{profile.unspent_points > 0 && (
<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>
</div>
)}
@@ -191,28 +196,28 @@ function PlayerSidebar({
{/* Compact 2x2 Stats Grid */}
<div className="stats-grid">
<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>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('strength')}>+</button>
)}
</div>
<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>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('agility')}>+</button>
)}
</div>
<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>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('endurance')}>+</button>
)}
</div>
<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>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('intellect')}>+</button>
@@ -225,7 +230,7 @@ function PlayerSidebar({
{/* Inventory Capacity - matching HP/Stamina/XP style */}
<div className="sidebar-stat-bar">
<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>
</div>
<div className="sidebar-progress-bar">
@@ -239,7 +244,7 @@ function PlayerSidebar({
<div className="sidebar-stat-bar">
<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>
</div>
<div className="sidebar-progress-bar">
@@ -271,7 +276,7 @@ function PlayerSidebar({
transition: 'all 0.2s'
}}
>
🎒 Open Inventory
{t('game.inventory')}
</button>
</div>
)}
@@ -279,24 +284,24 @@ function PlayerSidebar({
{/* Equipment Display - Proper Grid Layout */}
<div className="equipment-sidebar">
<h3> Equipment</h3>
<h3>{t('game.equipment')}</h3>
<div className="equipment-grid">
{/* Row 1: Head */}
<div className="equipment-row">
{renderEquipmentSlot('head', equipment.head, '🪖', 'Head')}
{renderEquipmentSlot('head', equipment.head, '🪖', t('equipment.head'))}
</div>
{/* Row 2: Weapon, Torso, Backpack */}
<div className="equipment-row three-cols">
{renderEquipmentSlot('weapon', equipment.weapon, '⚔️', 'Weapon')}
{renderEquipmentSlot('torso', equipment.torso, '👕', 'Torso')}
{renderEquipmentSlot('backpack', equipment.backpack, '🎒', 'Backpack')}
{renderEquipmentSlot('weapon', equipment.weapon, '⚔️', t('equipment.weapon'))}
{renderEquipmentSlot('torso', equipment.torso, '👕', t('equipment.torso'))}
{renderEquipmentSlot('backpack', equipment.backpack, '🎒', t('equipment.backpack'))}
</div>
{/* Row 3: Legs & Feet */}
<div className="equipment-row two-cols">
{renderEquipmentSlot('legs', equipment.legs, '👖', 'Legs')}
{renderEquipmentSlot('feet', equipment.feet, '👟', 'Feet')}
{renderEquipmentSlot('legs', equipment.legs, '👖', t('equipment.legs'))}
{renderEquipmentSlot('feet', equipment.feet, '👟', t('equipment.feet'))}
</div>
</div>
</div>

View File

@@ -1,5 +1,8 @@
import { useState, useEffect, type MouseEvent, type ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next'
import type { Profile, WorkbenchTab } from './types'
import { getAssetPath } from '../../utils/assetPath'
import { getTranslatedText } from '../../utils/i18nUtils'
import './Workbench.css'
interface WorkbenchProps {
@@ -47,6 +50,8 @@ function Workbench({
onRepair,
onUncraft
}: WorkbenchProps) {
useTranslation()
const [selectedItem, setSelectedItem] = useState<any>(null)
// Reset selection when tab changes
@@ -84,12 +89,12 @@ function Workbench({
switch (workbenchTab) {
case 'craft':
return craftableItems.filter(item =>
item.name.toLowerCase().includes(craftFilter.toLowerCase()) &&
getTranslatedText(item.name).toLowerCase().includes(craftFilter.toLowerCase()) &&
(craftCategoryFilter === 'all' || item.category === craftCategoryFilter)
)
case 'repair':
return repairableItems
.filter(item => item.name.toLowerCase().includes(repairFilter.toLowerCase()))
.filter(item => getTranslatedText(item.name).toLowerCase().includes(repairFilter.toLowerCase()))
.sort((a, b) => {
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':
return uncraftableItems.filter(item =>
item.name.toLowerCase().includes(uncraftFilter.toLowerCase())
getTranslatedText(item.name).toLowerCase().includes(uncraftFilter.toLowerCase())
)
default:
return []
@@ -118,7 +123,7 @@ function Workbench({
}
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 (
<>
@@ -127,7 +132,7 @@ function Workbench({
{imagePath ? (
<img
src={imagePath}
alt={item.name}
alt={getTranslatedText(item.name)}
className="detail-image"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
@@ -140,8 +145,9 @@ function Workbench({
{item.emoji || '📦'}
</div>
</div>
<h2 className="detail-title">{item.emoji} {item.name}</h2>
{item.description && <p className="detail-description">{item.description}</p>}
<div className="item-detail-header">
<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 */}
{workbenchTab === 'craft' && (item.base_stats || item.stats) && (
@@ -214,7 +220,7 @@ function Workbench({
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
{item.tools.map((tool: any, i: number) => (
<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>
{tool.has_tool ? `${tool.tool_durability}/${tool.max_durability} (Cost: ${tool.durability_cost})` : `❌ Missing (Cost: ${tool.durability_cost})`}
</span>
@@ -227,7 +233,7 @@ function Workbench({
{item.materials && item.materials.length > 0 ? (
item.materials.map((mat: any, i: number) => (
<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>
</div>
))
@@ -299,7 +305,7 @@ function Workbench({
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
{item.tools.map((tool: any, i: number) => (
<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>
{tool.has_tool ? `${tool.tool_durability}/${tool.max_durability} (Cost: ${tool.durability_cost})` : `❌ Missing (Cost: ${tool.durability_cost})`}
</span>
@@ -311,7 +317,7 @@ function Workbench({
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Materials</h5>
{item.materials.map((mat: any, i: number) => (
<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>
</div>
))}
@@ -388,7 +394,7 @@ function Workbench({
{adjustedYield.map((mat: any, i: number) => (
<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>
</div>
))}
@@ -403,7 +409,7 @@ function Workbench({
className="uncraft-btn"
disabled={(profile?.stamina || 0) < (item.stamina_cost || 1)}
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)
}
}}
@@ -417,6 +423,7 @@ function Workbench({
</div>
</>
)}
</div>
</>
)
}
@@ -500,7 +507,7 @@ function Workbench({
{items.filter(item => {
// Text search filter
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)
let matchesCategory = true
@@ -521,7 +528,7 @@ function Workbench({
.filter(item => {
// Text search filter
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)
let matchesCategory = true
@@ -533,7 +540,7 @@ function Workbench({
return matchesSearch && matchesCategory
})
.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 (
<div
key={item.unique_item_id || item.item_id || idx}
@@ -545,7 +552,7 @@ function Workbench({
{imagePath ? (
<img
src={imagePath}
alt={item.name}
alt={getTranslatedText(item.name)}
className="item-thumb-img"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
@@ -564,7 +571,7 @@ function Workbench({
<span
className={`item-name ${(workbenchTab === 'repair' || workbenchTab === 'uncraft') && item.tier ? `text-tier-${item.tier}` : ''}`}
>
{item.name}
{getTranslatedText(item.name)}
</span>
{item.location === 'equipped' && <span className="item-card-equipped" style={{ marginLeft: '0.5rem' }}>Equipped</span>}
</div>

View File

@@ -16,13 +16,13 @@ export interface DirectionDetail {
stamina_cost: number
distance: number
destination: string
destination_name?: string
destination_name?: string | { [key: string]: string }
}
export interface Location {
id: string
name: string
description: string
name: string | { [key: string]: string }
description: string | { [key: string]: string }
directions: string[]
directions_detailed?: DirectionDetail[]
danger_level?: number

29
pwa/src/i18n/index.ts Normal file
View 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

View 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"
}
}

View 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"
}
}

Some files were not shown because too many files have changed in this diff Show More