Commit
This commit is contained in:
@@ -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
323
web-map/player_endpoints_fixed.py
Normal file
323
web-map/player_endpoints_fixed.py
Normal 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
|
||||
1924
web-map/server.py
1924
web-map/server.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user