This commit is contained in:
Joan
2025-11-27 16:27:01 +01:00
parent 33cc9586c2
commit 81f8912059
304 changed files with 56149 additions and 10122 deletions

View File

@@ -36,6 +36,45 @@ Optional: Specify a custom port
python server.py --port 3000
```
## Map Editor
The server includes a built-in web-based editor for managing game data.
### Accessing the Editor
Navigate to: **http://localhost:8080/editor**
### Authentication
The editor requires a password for access. Set it via environment variable:
```bash
export EDITOR_PASSWORD="your_secure_password"
export EDITOR_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
```
**Default password (if not set):** `admin123`
> [!WARNING]
> **Security**: Always change the default password in production! Set `EDITOR_PASSWORD` in your `.env` file.
### Editor Features
- **Locations Tab**: Edit location properties, coordinates, danger levels, spawn tables
- **NPCs Tab**: Manage enemy stats, loot tables, and spawn weights
- **Items Tab**: Edit item properties, stats, crafting recipes, repair materials
- **Interactables Tab**: Manage interactable templates and actions
- **Connections Tab**: Create/delete connections between locations
- **Logs Tab**: View API container logs and restart the bot
### Environment Variables
| Variable | Description | Default |
|----------|-------------|----------|
| `EDITOR_PASSWORD` | Password for editor access | `admin123` |
| `EDITOR_SECRET_KEY` | Flask session secret key | Auto-generated |
| `PORT` | Server port | `8080` |
## Features Overview
### Map Controls
@@ -100,8 +139,10 @@ The map dynamically loads data from `/map_data.json`, which is generated from th
### Server Architecture
- **Backend**: Python HTTP server with dynamic data generation
- **Backend**: Flask server with RESTful API
- **Frontend**: Vanilla JavaScript with HTML5 Canvas
- **Authentication**: Session-based with password protection
- **Data Storage**: Direct JSON file manipulation
- **Responsive**: CSS Grid and Flexbox layout
- **Real-time**: Live data from game world loader
@@ -195,8 +236,22 @@ To modify the map visualization:
2. Edit `index.html` for layout and UI
3. Edit `server.py` for data serving logic
To modify the editor:
1. Edit `editor.html` for editor UI layout
2. Edit `editor_enhanced.js` for editor functionality
3. Edit `server.py` API routes for backend logic
The server auto-loads changes - just refresh your browser!
## Security Best Practices
1. **Change Default Password**: Always set `EDITOR_PASSWORD` to a strong password
2. **Use HTTPS**: In production, use a reverse proxy (Traefik/Nginx) with SSL
3. **Restrict Access**: Use firewall rules to limit editor access to trusted IPs
4. **Backup Data**: Regularly backup `gamedata/` folder before making changes
5. **Test Changes**: Use the export/import feature to test changes before applying
## License
Part of the Echoes of the Ashes RPG project.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,323 @@
# Player Management API Endpoints - Fixed for accounts+characters schema
@app.route('/api/editor/players', methods=['GET'])
@require_auth
def get_players():
"""Get list of all characters with their account info"""
sys.path.insert(0, str(PARENT_DIR))
try:
from api import database
except ImportError:
return jsonify({'error': 'Database module not available'}), 500
import asyncio
import time
from sqlalchemy import text
async def fetch_players():
players_list = []
try:
async with database.engine.connect() as conn:
# Get all characters with account info
result = await conn.execute(text("""
SELECT c.id, c.account_id, c.name, c.location_id, c.hp, c.max_hp,
c.stamina, c.max_stamina, c.level, c.xp, c.strength, c.agility,
c.endurance, c.intellect, c.unspent_points,
a.email, a.premium_expires_at, a.created_at, c.created_at as character_created_at
FROM characters c
LEFT JOIN accounts a ON c.account_id = a.id
ORDER BY c.name
"""))
rows = result.fetchall()
for row in rows:
players_list.append({
'id': row[0],
'account_id': row[1],
'character_name': row[2],
'location_id': row[3],
'hp': row[4],
'max_hp': row[5],
'stamina': row[6],
'max_stamina': row[7],
'level': row[8],
'xp': row[9],
'strength': row[10],
'agility': row[11],
'endurance': row[12],
'intellect': row[13],
'unspent_points': row[14],
'email': row[15],
'premium_expires_at': row[16],
'account_created_at': row[17],
'character_created_at': row[18],
'is_premium': row[16] and row[16] > time.time() if row[16] else False
})
except Exception as e:
print(f"[PLAYERS] Error fetching players: {e}", flush=True)
import traceback
traceback.print_exc()
return players_list
players = asyncio.run(fetch_players())
return jsonify({'players': players})
@app.route('/api/editor/player/<int:character_id>', methods=['GET'])
@require_auth
def get_player_details(character_id):
"""Get detailed character information including inventory"""
sys.path.insert(0, str(PARENT_DIR))
try:
from api import database
except ImportError:
return jsonify({'error': 'Database module not available'}), 500
import asyncio
from sqlalchemy import text
async def fetch_player_details():
player_data = {}
try:
async with database.engine.connect() as conn:
# Get character basic info with account
result = await conn.execute(text("""
SELECT c.*, a.email, a.premium_expires_at, a.created_at as account_created_at
FROM characters c
LEFT JOIN accounts a ON c.account_id = a.id
WHERE c.id = :cid
"""), {'cid': character_id})
row = result.fetchone()
if not row:
return None
player_data = dict(row._mapping)
# Get inventory
result = await conn.execute(text("""
SELECT i.item_id, i.quantity, i.is_equipped, i.unique_item_id,
u.durability, u.max_durability, u.tier, u.unique_stats
FROM inventory i
LEFT JOIN unique_items u ON i.unique_item_id = u.id
WHERE i.character_id = :cid
"""), {'cid': character_id})
inventory = []
equipped = {}
for row in result.fetchall():
item_data = {
'item_id': row[0],
'quantity': row[1]
}
if row[3]: # Has unique_item_id
item_data['unique_item_data'] = {
'durability': row[4],
'max_durability': row[5],
'tier': row[6],
'unique_stats': row[7]
}
if row[2]: # is_equipped
equipped[row[0]] = item_data
else:
inventory.append(item_data)
player_data['inventory'] = inventory
player_data['equipped'] = equipped
except Exception as e:
print(f"[PLAYER-DETAILS] Error: {e}", flush=True)
import traceback
traceback.print_exc()
return None
return player_data
player = asyncio.run(fetch_player_details())
if not player:
return jsonify({'error': 'Player not found'}), 404
return jsonify(player)
@app.route('/api/editor/player/<int:character_id>', methods=['POST'])
@require_auth
def update_player(character_id):
"""Update character stats and properties"""
sys.path.insert(0, str(PARENT_DIR))
try:
from api import database
except ImportError:
return jsonify({'error': 'Database module not available'}), 500
import asyncio
from sqlalchemy import text
data = request.get_json()
async def update_player_data():
try:
async with database.engine.begin() as conn:
# Update character stats
await conn.execute(text("""
UPDATE characters SET
name = :name,
location_id = :location,
hp = :hp,
max_hp = :max_hp,
stamina = :stamina,
max_stamina = :max_stamina,
level = :level,
xp = :xp,
strength = :str,
agility = :agi,
endurance = :end,
intellect = :int,
unspent_points = :unspent
WHERE id = :cid
"""), {
'cid': character_id,
'name': data.get('character_name'),
'location': data.get('location_id'),
'hp': data.get('hp'),
'max_hp': data.get('max_hp'),
'stamina': data.get('stamina'),
'max_stamina': data.get('max_stamina'),
'level': data.get('level'),
'xp': data.get('xp'),
'str': data.get('strength'),
'agi': data.get('agility'),
'end': data.get('endurance'),
'int': data.get('intellect'),
'unspent': data.get('unspent_points', 0)
})
return True
except Exception as e:
print(f"[UPDATE-PLAYER] Error: {e}", flush=True)
import traceback
traceback.print_exc()
return False
success = asyncio.run(update_player_data())
if success:
return jsonify({'success': True, 'message': 'Player updated successfully'})
else:
return jsonify({'error': 'Failed to update player'}), 500
# Simplified inventory/equipment endpoints - not fully implemented
@app.route('/api/editor/player/<int:character_id>/inventory', methods=['POST'])
@require_auth
def update_player_inventory(character_id):
return jsonify({'success': True, 'message': 'Inventory editing not yet implemented'})
@app.route('/api/editor/player/<int:character_id>/equipment', methods=['POST'])
@require_auth
def update_player_equipment(character_id):
return jsonify({'success': True, 'message': 'Equipment managed via inventory'})
# Simplified account management - accounts don't have ban functionality in new schema
@app.route('/api/editor/account/<int:account_id>/ban', methods=['POST'])
@require_auth
def ban_account(account_id):
return jsonify({'success': True, 'message': 'Ban functionality not implemented in new schema'})
@app.route('/api/editor/account/<int:account_id>/delete', methods=['DELETE'])
@require_auth
def delete_account(account_id):
"""Delete an account and all associated characters"""
sys.path.insert(0, str(PARENT_DIR))
try:
from api import database
except ImportError:
return jsonify({'error': 'Database module not available'}), 500
import asyncio
from sqlalchemy import text
async def delete_account_data():
try:
async with database.engine.begin() as conn:
# CASCADE will handle characters and their inventory
await conn.execute(text("DELETE FROM accounts WHERE id = :aid"), {'aid': account_id})
return True
except Exception as e:
print(f"[DELETE-ACCOUNT] Error: {e}", flush=True)
import traceback
traceback.print_exc()
return False
success = asyncio.run(delete_account_data())
if success:
return jsonify({'success': True, 'message': 'Account deleted successfully'})
else:
return jsonify({'error': 'Failed to delete account'}), 500
@app.route('/api/editor/player/<int:character_id>/reset', methods=['POST'])
@require_auth
def reset_player(character_id):
"""Reset character to starting state"""
sys.path.insert(0, str(PARENT_DIR))
try:
from api import database
except ImportError:
return jsonify({'error': 'Database module not available'}), 500
import asyncio
from sqlalchemy import text
async def reset_player_data():
try:
async with database.engine.begin() as conn:
# Clear inventory
await conn.execute(text("DELETE FROM inventory WHERE character_id = :cid"), {'cid': character_id})
# Reset character stats to defaults
await conn.execute(text("""
UPDATE characters SET
location_id = 'cabin',
hp = 100,
max_hp = 100,
stamina = 100,
max_stamina = 100,
level = 1,
xp = 0,
strength = 0,
agility = 0,
endurance = 0,
intellect = 0,
unspent_points = 20,
is_dead = false
WHERE id = :cid
"""), {'cid': character_id})
return True
except Exception as e:
print(f"[RESET-PLAYER] Error: {e}", flush=True)
import traceback
traceback.print_exc()
return False
success = asyncio.run(reset_player_data())
if success:
return jsonify({'success': True, 'message': 'Player reset successfully'})
else:
return jsonify({'error': 'Failed to reset player'}), 500

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff