feat: pivot to island survival simulation with SQLite persistence
Phase 3 - "The Island" transformation: - Add SQLAlchemy + SQLite for data persistence (database.py) - Rewrite models.py with User, Agent, WorldState ORM models - Refactor engine.py for survival mechanics (energy decay, starvation) - Implement feed command (10 gold -> +20 energy) - Auto-seed 3 NPCs on startup (Jack/Luna/Bob) - Update frontend with agent card view and Chinese UI - Remove old Boss/Player RPG mechanics - Add .gitignore for database and cache files - Fix SQLAlchemy session detachment issue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# Virtual environment
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
"""
|
|
||||||
Agent classes for processing game inputs and generating responses.
|
|
||||||
Provides base abstraction and rule-based implementation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent(ABC):
|
|
||||||
"""
|
|
||||||
Abstract base class for all game agents.
|
|
||||||
|
|
||||||
Agents process input text and generate appropriate responses.
|
|
||||||
Subclasses must implement the process_input method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name: str = "BaseAgent") -> None:
|
|
||||||
"""
|
|
||||||
Initialize the agent.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Display name for the agent
|
|
||||||
"""
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def process_input(self, text: str) -> str:
|
|
||||||
"""
|
|
||||||
Process input text and return a response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The input text to process
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The agent's response string
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RuleBasedAgent(BaseAgent):
|
|
||||||
"""
|
|
||||||
A simple rule-based agent that responds based on keyword matching.
|
|
||||||
|
|
||||||
This agent does not use LLMs - it returns static strings based on
|
|
||||||
detected keywords in the input text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name: str = "RuleBot") -> None:
|
|
||||||
"""Initialize the rule-based agent."""
|
|
||||||
super().__init__(name)
|
|
||||||
self._rules: dict[str, str] = {
|
|
||||||
"attack": "Defending! Shield activated!",
|
|
||||||
"heal": "Healing spell cast! +50 HP",
|
|
||||||
"run": "Too slow! You can't escape!",
|
|
||||||
"help": "Allies are on the way!",
|
|
||||||
"fire": "Water shield deployed!",
|
|
||||||
"magic": "Counter-spell activated!",
|
|
||||||
}
|
|
||||||
self._default_response = "I heard you! Processing command..."
|
|
||||||
|
|
||||||
def process_input(self, text: str) -> str:
|
|
||||||
"""
|
|
||||||
Process input and return response based on keyword rules.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The input text to analyze
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response string based on matched keywords
|
|
||||||
"""
|
|
||||||
text_lower = text.lower()
|
|
||||||
|
|
||||||
for keyword, response in self._rules.items():
|
|
||||||
if keyword in text_lower:
|
|
||||||
logger.info(f"Agent matched keyword: {keyword}")
|
|
||||||
return f"{self.name}: {response}"
|
|
||||||
|
|
||||||
logger.info("Agent using default response")
|
|
||||||
return f"{self.name}: {self._default_response}"
|
|
||||||
61
backend/app/database.py
Normal file
61
backend/app/database.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Database configuration for The Island.
|
||||||
|
SQLite + SQLAlchemy ORM setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
|
||||||
|
# Database file path (in project root)
|
||||||
|
DB_PATH = Path(__file__).parent.parent.parent / "island.db"
|
||||||
|
DATABASE_URL = f"sqlite:///{DB_PATH}"
|
||||||
|
|
||||||
|
# Create engine with SQLite optimizations
|
||||||
|
engine = create_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
connect_args={"check_same_thread": False}, # Required for SQLite with FastAPI
|
||||||
|
echo=False # Set True for SQL debugging
|
||||||
|
)
|
||||||
|
|
||||||
|
# Session factory
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Base class for ORM models
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Initialize database tables."""
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
"""
|
||||||
|
Dependency for getting database session.
|
||||||
|
Use with FastAPI's Depends() or as context manager.
|
||||||
|
"""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_db_session():
|
||||||
|
"""
|
||||||
|
Context manager for database session.
|
||||||
|
Usage: with get_db_session() as db: ...
|
||||||
|
"""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
Core Game Engine - The "Heartbeat" of the game.
|
Core Game Engine - The Island Survival Simulation.
|
||||||
Manages game state, processes commands, and coordinates broadcasts.
|
Manages survival mechanics, agent states, and user interactions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -10,35 +10,43 @@ import time
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .schemas import GameEvent, EventType
|
from .schemas import GameEvent, EventType
|
||||||
from .models import Player, Boss
|
from .database import init_db, get_db_session
|
||||||
|
from .models import User, Agent, WorldState
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .server import ConnectionManager
|
from .server import ConnectionManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Command patterns for string matching
|
# Command patterns
|
||||||
ATTACK_PATTERN = re.compile(r"(attack|攻击|打|砍|杀)", re.IGNORECASE)
|
FEED_PATTERN = re.compile(r"feed\s+(\w+)", re.IGNORECASE)
|
||||||
HEAL_PATTERN = re.compile(r"(heal|治疗|回血|加血|恢复)", re.IGNORECASE)
|
CHECK_PATTERN = re.compile(r"(check|查询|状态)", re.IGNORECASE)
|
||||||
STATUS_PATTERN = re.compile(r"(status|查询|状态|信息)", re.IGNORECASE)
|
|
||||||
|
|
||||||
# Game constants
|
# Game constants
|
||||||
ATTACK_DAMAGE = 10
|
TICK_INTERVAL = 5.0 # Seconds between ticks
|
||||||
ATTACK_GOLD_REWARD = 10
|
ENERGY_DECAY_PER_TICK = 2 # Energy lost per tick
|
||||||
HEAL_AMOUNT = 10
|
HP_DECAY_WHEN_STARVING = 5 # HP lost when energy is 0
|
||||||
BOSS_COUNTER_DAMAGE = 15 # Boss反击伤害
|
FEED_COST = 10 # Gold cost to feed an agent
|
||||||
|
FEED_ENERGY_RESTORE = 20 # Energy restored when fed
|
||||||
|
INITIAL_USER_GOLD = 100 # Starting gold for new users
|
||||||
|
|
||||||
|
# Initial NPC data
|
||||||
|
INITIAL_AGENTS = [
|
||||||
|
{"name": "Jack", "personality": "勇敢"},
|
||||||
|
{"name": "Luna", "personality": "狡猾"},
|
||||||
|
{"name": "Bob", "personality": "老实"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class GameEngine:
|
class GameEngine:
|
||||||
"""
|
"""
|
||||||
The core game engine that manages RPG state and game loop.
|
The core game engine for island survival simulation.
|
||||||
|
Manages agents, users, and survival mechanics with database persistence.
|
||||||
Manages players, boss, and processes commands through string matching.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, connection_manager: "ConnectionManager") -> None:
|
def __init__(self, connection_manager: "ConnectionManager") -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the game engine with state storage.
|
Initialize the game engine.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
connection_manager: The WebSocket connection manager for broadcasting
|
connection_manager: The WebSocket connection manager for broadcasting
|
||||||
@@ -46,40 +54,50 @@ class GameEngine:
|
|||||||
self._manager = connection_manager
|
self._manager = connection_manager
|
||||||
self._running = False
|
self._running = False
|
||||||
self._tick_count = 0
|
self._tick_count = 0
|
||||||
self._tick_interval = 2.0 # seconds
|
self._tick_interval = TICK_INTERVAL
|
||||||
|
|
||||||
# Game state
|
|
||||||
self.players: dict[str, Player] = {}
|
|
||||||
self.boss = Boss(name="Dragon", hp=1000, max_hp=1000)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
"""Check if the engine is currently running."""
|
"""Check if the engine is currently running."""
|
||||||
return self._running
|
return self._running
|
||||||
|
|
||||||
def _get_or_create_player(self, username: str) -> Player:
|
def _seed_initial_data(self) -> None:
|
||||||
"""
|
"""Seed initial agents and world state if database is empty."""
|
||||||
Get existing player or create new one.
|
with get_db_session() as db:
|
||||||
|
# Check if agents exist
|
||||||
|
agent_count = db.query(Agent).count()
|
||||||
|
if agent_count == 0:
|
||||||
|
logger.info("Seeding initial agents...")
|
||||||
|
for agent_data in INITIAL_AGENTS:
|
||||||
|
agent = Agent(
|
||||||
|
name=agent_data["name"],
|
||||||
|
personality=agent_data["personality"],
|
||||||
|
status="Alive",
|
||||||
|
hp=100,
|
||||||
|
energy=100
|
||||||
|
)
|
||||||
|
db.add(agent)
|
||||||
|
logger.info(f"Created {len(INITIAL_AGENTS)} initial agents")
|
||||||
|
|
||||||
Args:
|
# Check if world state exists
|
||||||
username: The player's username
|
world = db.query(WorldState).first()
|
||||||
|
if world is None:
|
||||||
|
logger.info("Seeding initial world state...")
|
||||||
|
world = WorldState(day_count=1, weather="Sunny", resource_level=100)
|
||||||
|
db.add(world)
|
||||||
|
|
||||||
Returns:
|
def _get_or_create_user(self, db, username: str) -> User:
|
||||||
Player instance
|
"""Get existing user or create new one."""
|
||||||
"""
|
user = db.query(User).filter(User.username == username).first()
|
||||||
if username not in self.players:
|
if user is None:
|
||||||
self.players[username] = Player(name=username)
|
user = User(username=username, gold=INITIAL_USER_GOLD)
|
||||||
logger.info(f"New player registered: {username}")
|
db.add(user)
|
||||||
return self.players[username]
|
db.flush() # Get the ID without committing
|
||||||
|
logger.info(f"New user registered: {username}")
|
||||||
|
return user
|
||||||
|
|
||||||
async def _broadcast_event(self, event_type: str, data: dict) -> None:
|
async def _broadcast_event(self, event_type: str, data: dict) -> None:
|
||||||
"""
|
"""Create and broadcast a game event."""
|
||||||
Create and broadcast a game event.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_type: Type of the event
|
|
||||||
data: Event payload data
|
|
||||||
"""
|
|
||||||
event = GameEvent(
|
event = GameEvent(
|
||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
timestamp=time.time(),
|
timestamp=time.time(),
|
||||||
@@ -87,216 +105,245 @@ class GameEngine:
|
|||||||
)
|
)
|
||||||
await self._manager.broadcast(event)
|
await self._manager.broadcast(event)
|
||||||
|
|
||||||
async def _broadcast_boss_update(self) -> None:
|
async def _broadcast_agents_status(self) -> None:
|
||||||
"""Broadcast current boss status to all clients."""
|
"""Broadcast all agents' current status."""
|
||||||
|
with get_db_session() as db:
|
||||||
|
agents = db.query(Agent).all()
|
||||||
|
agents_data = [agent.to_dict() for agent in agents]
|
||||||
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.BOSS_UPDATE,
|
EventType.AGENTS_UPDATE,
|
||||||
|
{"agents": agents_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _process_survival_tick(self) -> None:
|
||||||
|
"""
|
||||||
|
Process survival mechanics for all alive agents.
|
||||||
|
- Decrease energy
|
||||||
|
- If energy <= 0, decrease HP
|
||||||
|
- If HP <= 0, mark as Dead
|
||||||
|
"""
|
||||||
|
deaths = []
|
||||||
|
|
||||||
|
with get_db_session() as db:
|
||||||
|
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||||
|
|
||||||
|
for agent in alive_agents:
|
||||||
|
# Energy decay
|
||||||
|
agent.energy = max(0, agent.energy - ENERGY_DECAY_PER_TICK)
|
||||||
|
|
||||||
|
# Starvation damage
|
||||||
|
if agent.energy <= 0:
|
||||||
|
agent.hp = max(0, agent.hp - HP_DECAY_WHEN_STARVING)
|
||||||
|
|
||||||
|
# Check for death
|
||||||
|
if agent.hp <= 0:
|
||||||
|
agent.status = "Dead"
|
||||||
|
deaths.append({
|
||||||
|
"name": agent.name,
|
||||||
|
"personality": agent.personality
|
||||||
|
})
|
||||||
|
logger.info(f"Agent {agent.name} has died!")
|
||||||
|
|
||||||
|
# Broadcast death events
|
||||||
|
for death in deaths:
|
||||||
|
await self._broadcast_event(
|
||||||
|
EventType.AGENT_DIED,
|
||||||
{
|
{
|
||||||
"boss_name": self.boss.name,
|
"agent_name": death["name"],
|
||||||
"boss_hp": self.boss.hp,
|
"message": f"💀 {death['name']}({death['personality']})因饥饿而死亡..."
|
||||||
"boss_max_hp": self.boss.max_hp,
|
|
||||||
"boss_hp_percentage": self.boss.hp_percentage
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_attack(self, player: Player) -> None:
|
async def _handle_feed(self, username: str, agent_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Handle attack command. Boss will counter-attack!
|
Handle feed command.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
player: The attacking player
|
username: The user feeding the agent
|
||||||
|
agent_name: Name of the agent to feed
|
||||||
"""
|
"""
|
||||||
if not self.boss.is_alive:
|
# Variables to store data for broadcasting (outside session)
|
||||||
|
feed_result = None
|
||||||
|
|
||||||
|
with get_db_session() as db:
|
||||||
|
user = self._get_or_create_user(db, username)
|
||||||
|
|
||||||
|
# Find the agent
|
||||||
|
agent = db.query(Agent).filter(
|
||||||
|
Agent.name.ilike(agent_name)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if agent is None:
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.SYSTEM,
|
EventType.ERROR,
|
||||||
{"message": f"Boss is already defeated! Waiting for respawn..."}
|
{"message": f"找不到名为 {agent_name} 的角色"}
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Player attacks boss
|
if agent.status != "Alive":
|
||||||
damage = self.boss.take_damage(ATTACK_DAMAGE)
|
|
||||||
player.add_gold(ATTACK_GOLD_REWARD)
|
|
||||||
|
|
||||||
# Boss counter-attacks player
|
|
||||||
counter_damage = player.take_damage(BOSS_COUNTER_DAMAGE)
|
|
||||||
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.ATTACK,
|
EventType.ERROR,
|
||||||
{
|
{"message": f"{agent.name} 已经死亡,无法投喂"}
|
||||||
"user": player.name,
|
|
||||||
"damage": damage,
|
|
||||||
"counter_damage": counter_damage,
|
|
||||||
"gold_earned": ATTACK_GOLD_REWARD,
|
|
||||||
"boss_hp": self.boss.hp,
|
|
||||||
"boss_max_hp": self.boss.max_hp,
|
|
||||||
"player_hp": player.hp,
|
|
||||||
"player_max_hp": player.max_hp,
|
|
||||||
"player_gold": player.gold,
|
|
||||||
"message": f"⚔️ {player.name} attacked {self.boss.name}! "
|
|
||||||
f"Dealt {damage} dmg, received {counter_damage} counter-attack. "
|
|
||||||
f"HP: {player.hp}/{player.max_hp} | Boss: {self.boss.hp}/{self.boss.max_hp}"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._broadcast_boss_update()
|
|
||||||
|
|
||||||
# Check if player died
|
|
||||||
if not player.is_alive:
|
|
||||||
await self._handle_player_death(player)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if boss defeated
|
if user.gold < FEED_COST:
|
||||||
if not self.boss.is_alive:
|
|
||||||
await self._handle_boss_defeated()
|
|
||||||
|
|
||||||
async def _handle_player_death(self, player: Player) -> None:
|
|
||||||
"""Handle player death - respawn with full HP but lose half gold."""
|
|
||||||
lost_gold = player.gold // 2
|
|
||||||
player.gold -= lost_gold
|
|
||||||
player.hp = player.max_hp # Respawn with full HP
|
|
||||||
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.SYSTEM,
|
EventType.ERROR,
|
||||||
{
|
{
|
||||||
"user": player.name,
|
"user": username,
|
||||||
"player_hp": player.hp,
|
"message": f"金币不足!需要 {FEED_COST} 金币,当前只有 {user.gold} 金币"
|
||||||
"player_max_hp": player.max_hp,
|
}
|
||||||
"player_gold": player.gold,
|
)
|
||||||
"message": f"💀 {player.name} was slain by {self.boss.name}! "
|
return
|
||||||
f"Lost {lost_gold} gold. Respawned with full HP."
|
|
||||||
|
# Perform feed
|
||||||
|
user.gold -= FEED_COST
|
||||||
|
old_energy = agent.energy
|
||||||
|
agent.energy = min(100, agent.energy + FEED_ENERGY_RESTORE)
|
||||||
|
actual_restore = agent.energy - old_energy
|
||||||
|
|
||||||
|
# Store data for broadcasting before session closes
|
||||||
|
feed_result = {
|
||||||
|
"agent_name": agent.name,
|
||||||
|
"actual_restore": actual_restore,
|
||||||
|
"agent_energy": agent.energy,
|
||||||
|
"user_gold": user.gold
|
||||||
|
}
|
||||||
|
|
||||||
|
# Broadcast outside of session
|
||||||
|
if feed_result:
|
||||||
|
await self._broadcast_event(
|
||||||
|
EventType.FEED,
|
||||||
|
{
|
||||||
|
"user": username,
|
||||||
|
"agent_name": feed_result["agent_name"],
|
||||||
|
"energy_restored": feed_result["actual_restore"],
|
||||||
|
"agent_energy": feed_result["agent_energy"],
|
||||||
|
"user_gold": feed_result["user_gold"],
|
||||||
|
"message": f"🍖 {username} 投喂了 {feed_result['agent_name']}!"
|
||||||
|
f"恢复 {feed_result['actual_restore']} 点体力(当前: {feed_result['agent_energy']}/100)"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_boss_defeated(self) -> None:
|
|
||||||
"""Handle boss defeat and reset."""
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.BOSS_DEFEATED,
|
EventType.USER_UPDATE,
|
||||||
{
|
{
|
||||||
"boss_name": self.boss.name,
|
"user": username,
|
||||||
"message": f"🎉 {self.boss.name} has been defeated! A new boss will spawn soon..."
|
"gold": feed_result["user_gold"]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset boss after short delay
|
async def _handle_check(self, username: str) -> None:
|
||||||
await asyncio.sleep(3.0)
|
"""Handle check/status command."""
|
||||||
self.boss.reset()
|
with get_db_session() as db:
|
||||||
await self._broadcast_event(
|
user = self._get_or_create_user(db, username)
|
||||||
EventType.SYSTEM,
|
agents = db.query(Agent).all()
|
||||||
{"message": f"⚔️ {self.boss.name} has respawned with full HP!"}
|
world = db.query(WorldState).first()
|
||||||
)
|
|
||||||
await self._broadcast_boss_update()
|
|
||||||
|
|
||||||
async def _handle_heal(self, player: Player) -> None:
|
# Extract data before session closes
|
||||||
"""
|
user_data = {"username": user.username, "gold": user.gold}
|
||||||
Handle heal command.
|
agents_data = [agent.to_dict() for agent in agents]
|
||||||
|
world_data = world.to_dict() if world else {}
|
||||||
Args:
|
message = f"📊 {username} 的状态 - 金币: {user_data['gold']}"
|
||||||
player: The healing player
|
|
||||||
"""
|
|
||||||
healed = player.heal(HEAL_AMOUNT)
|
|
||||||
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.HEAL,
|
EventType.CHECK,
|
||||||
{
|
{
|
||||||
"user": player.name,
|
"user": user_data,
|
||||||
"healed": healed,
|
"agents": agents_data,
|
||||||
"player_hp": player.hp,
|
"world": world_data,
|
||||||
"player_max_hp": player.max_hp,
|
"message": message
|
||||||
"message": f"{player.name} healed themselves. "
|
|
||||||
f"Restored {healed} HP. HP: {player.hp}/{player.max_hp}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _handle_status(self, player: Player) -> None:
|
|
||||||
"""
|
|
||||||
Handle status query command.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
player: The querying player
|
|
||||||
"""
|
|
||||||
await self._broadcast_event(
|
|
||||||
EventType.STATUS,
|
|
||||||
{
|
|
||||||
"user": player.name,
|
|
||||||
"player_hp": player.hp,
|
|
||||||
"player_max_hp": player.max_hp,
|
|
||||||
"player_gold": player.gold,
|
|
||||||
"boss_name": self.boss.name,
|
|
||||||
"boss_hp": self.boss.hp,
|
|
||||||
"boss_max_hp": self.boss.max_hp,
|
|
||||||
"message": f"📊 {player.name}'s Status - HP: {player.hp}/{player.max_hp}, "
|
|
||||||
f"Gold: {player.gold} | Boss {self.boss.name}: {self.boss.hp}/{self.boss.max_hp}"
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def process_comment(self, user: str, message: str) -> None:
|
async def process_comment(self, user: str, message: str) -> None:
|
||||||
"""
|
"""
|
||||||
Process a comment through string matching command system.
|
Process a comment through command matching.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user: Username of the commenter
|
user: Username of the commenter
|
||||||
message: The comment text
|
message: The comment text
|
||||||
"""
|
"""
|
||||||
# Get or create player
|
# Broadcast the incoming comment
|
||||||
player = self._get_or_create_player(user)
|
|
||||||
|
|
||||||
# Broadcast the incoming comment first
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.COMMENT,
|
EventType.COMMENT,
|
||||||
{"user": user, "message": message}
|
{"user": user, "message": message}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process command through string matching
|
# Match commands
|
||||||
if ATTACK_PATTERN.search(message):
|
feed_match = FEED_PATTERN.search(message)
|
||||||
await self._handle_attack(player)
|
if feed_match:
|
||||||
elif HEAL_PATTERN.search(message):
|
agent_name = feed_match.group(1)
|
||||||
await self._handle_heal(player)
|
await self._handle_feed(user, agent_name)
|
||||||
elif STATUS_PATTERN.search(message):
|
return
|
||||||
await self._handle_status(player)
|
|
||||||
# If no command matched, treat as regular chat (no action needed)
|
if CHECK_PATTERN.search(message):
|
||||||
|
await self._handle_check(user)
|
||||||
|
return
|
||||||
|
|
||||||
|
# No command matched - treat as regular chat
|
||||||
|
|
||||||
async def _game_loop(self) -> None:
|
async def _game_loop(self) -> None:
|
||||||
"""
|
"""
|
||||||
The main game loop - runs continuously while engine is active.
|
The main game loop - survival simulation.
|
||||||
|
|
||||||
Every tick broadcasts current game state.
|
Every tick:
|
||||||
|
1. Process survival mechanics (energy/HP decay)
|
||||||
|
2. Broadcast agent states
|
||||||
"""
|
"""
|
||||||
logger.info("Game loop started")
|
logger.info("Game loop started - Island survival simulation")
|
||||||
|
|
||||||
# Broadcast initial boss state
|
# Initial broadcast
|
||||||
await self._broadcast_boss_update()
|
await self._broadcast_agents_status()
|
||||||
|
|
||||||
while self._running:
|
while self._running:
|
||||||
self._tick_count += 1
|
self._tick_count += 1
|
||||||
|
|
||||||
# Broadcast tick event with game state
|
# Process survival mechanics
|
||||||
|
await self._process_survival_tick()
|
||||||
|
|
||||||
|
# Broadcast current state
|
||||||
|
await self._broadcast_agents_status()
|
||||||
|
|
||||||
|
# Broadcast tick event
|
||||||
|
with get_db_session() as db:
|
||||||
|
alive_count = db.query(Agent).filter(Agent.status == "Alive").count()
|
||||||
|
world = db.query(WorldState).first()
|
||||||
|
day = world.day_count if world else 1
|
||||||
|
|
||||||
await self._broadcast_event(
|
await self._broadcast_event(
|
||||||
EventType.TICK,
|
EventType.TICK,
|
||||||
{
|
{
|
||||||
"tick": self._tick_count,
|
"tick": self._tick_count,
|
||||||
"boss_hp": self.boss.hp,
|
"day": day,
|
||||||
"boss_max_hp": self.boss.max_hp,
|
"alive_agents": alive_count
|
||||||
"player_count": len(self.players)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Tick {self._tick_count}: Boss HP {self.boss.hp}/{self.boss.max_hp}")
|
logger.debug(f"Tick {self._tick_count}: {alive_count} agents alive")
|
||||||
|
|
||||||
await asyncio.sleep(self._tick_interval)
|
await asyncio.sleep(self._tick_interval)
|
||||||
|
|
||||||
logger.info("Game loop stopped")
|
logger.info("Game loop stopped")
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start the game engine loop as a background task."""
|
"""Start the game engine."""
|
||||||
if self._running:
|
if self._running:
|
||||||
logger.warning("Engine already running")
|
logger.warning("Engine already running")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Initialize database and seed data
|
||||||
|
logger.info("Initializing database...")
|
||||||
|
init_db()
|
||||||
|
self._seed_initial_data()
|
||||||
|
|
||||||
self._running = True
|
self._running = True
|
||||||
asyncio.create_task(self._game_loop())
|
asyncio.create_task(self._game_loop())
|
||||||
logger.info("Game engine started")
|
logger.info("Game engine started - The Island awaits...")
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop the game engine loop."""
|
"""Stop the game engine."""
|
||||||
self._running = False
|
self._running = False
|
||||||
logger.info("Game engine stopping...")
|
logger.info("Game engine stopping...")
|
||||||
|
|||||||
@@ -1,66 +1,81 @@
|
|||||||
"""
|
"""
|
||||||
Game entity models for The Island.
|
SQLAlchemy ORM models for The Island.
|
||||||
Defines Player and Boss data structures.
|
Defines User (viewers), Agent (NPCs), and WorldState entities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime, func
|
||||||
|
|
||||||
|
from .database import Base
|
||||||
|
|
||||||
|
|
||||||
class Player(BaseModel):
|
class User(Base):
|
||||||
"""
|
"""
|
||||||
Represents a player in the game world.
|
Represents a viewer/donor from the live stream.
|
||||||
|
They can spend gold to influence the island.
|
||||||
"""
|
"""
|
||||||
name: str
|
__tablename__ = "users"
|
||||||
hp: int = Field(default=100, ge=0)
|
|
||||||
max_hp: int = Field(default=100, gt=0)
|
|
||||||
gold: int = Field(default=0, ge=0)
|
|
||||||
|
|
||||||
def take_damage(self, amount: int) -> int:
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
"""Apply damage to player, returns actual damage dealt."""
|
username = Column(String(100), unique=True, index=True, nullable=False)
|
||||||
actual = min(amount, self.hp)
|
gold = Column(Integer, default=100) # Starting gold for new users
|
||||||
self.hp -= actual
|
created_at = Column(DateTime, default=func.now())
|
||||||
return actual
|
|
||||||
|
|
||||||
def heal(self, amount: int) -> int:
|
def __repr__(self):
|
||||||
"""Heal player, returns actual HP restored."""
|
return f"<User {self.username} gold={self.gold}>"
|
||||||
actual = min(amount, self.max_hp - self.hp)
|
|
||||||
self.hp += actual
|
|
||||||
return actual
|
|
||||||
|
|
||||||
def add_gold(self, amount: int) -> None:
|
|
||||||
"""Add gold to player."""
|
|
||||||
self.gold += amount
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_alive(self) -> bool:
|
|
||||||
"""Check if player is alive."""
|
|
||||||
return self.hp > 0
|
|
||||||
|
|
||||||
|
|
||||||
class Boss(BaseModel):
|
class Agent(Base):
|
||||||
"""
|
"""
|
||||||
Represents a boss enemy in the game.
|
Represents an NPC survivor on the island.
|
||||||
|
Has personality, health, energy, and inventory.
|
||||||
"""
|
"""
|
||||||
name: str
|
__tablename__ = "agents"
|
||||||
hp: int = Field(ge=0)
|
|
||||||
max_hp: int = Field(gt=0)
|
|
||||||
|
|
||||||
def take_damage(self, amount: int) -> int:
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
"""Apply damage to boss, returns actual damage dealt."""
|
name = Column(String(50), unique=True, nullable=False)
|
||||||
actual = min(amount, self.hp)
|
personality = Column(String(50), nullable=False)
|
||||||
self.hp -= actual
|
status = Column(String(20), default="Alive") # Alive, Exiled, Dead
|
||||||
return actual
|
hp = Column(Integer, default=100)
|
||||||
|
energy = Column(Integer, default=100)
|
||||||
|
inventory = Column(String(500), default="{}") # JSON string
|
||||||
|
|
||||||
def reset(self) -> None:
|
def __repr__(self):
|
||||||
"""Reset boss to full HP."""
|
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Status={self.status}>"
|
||||||
self.hp = self.max_hp
|
|
||||||
|
|
||||||
@property
|
def to_dict(self):
|
||||||
def is_alive(self) -> bool:
|
"""Convert to dictionary for JSON serialization."""
|
||||||
"""Check if boss is alive."""
|
return {
|
||||||
return self.hp > 0
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"personality": self.personality,
|
||||||
|
"status": self.status,
|
||||||
|
"hp": self.hp,
|
||||||
|
"energy": self.energy,
|
||||||
|
"inventory": self.inventory
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
|
||||||
def hp_percentage(self) -> float:
|
class WorldState(Base):
|
||||||
"""Get HP as percentage."""
|
"""
|
||||||
return (self.hp / self.max_hp) * 100 if self.max_hp > 0 else 0
|
Global state of the island environment.
|
||||||
|
Tracks day count, weather, and shared resources.
|
||||||
|
"""
|
||||||
|
__tablename__ = "world_state"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
day_count = Column(Integer, default=1)
|
||||||
|
weather = Column(String(20), default="Sunny")
|
||||||
|
resource_level = Column(Integer, default=100)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<WorldState Day={self.day_count} Weather={self.weather} Resources={self.resource_level}>"
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert to dictionary for JSON serialization."""
|
||||||
|
return {
|
||||||
|
"day_count": self.day_count,
|
||||||
|
"weather": self.weather,
|
||||||
|
"resource_level": self.resource_level
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,17 +12,16 @@ import time
|
|||||||
class EventType(str, Enum):
|
class EventType(str, Enum):
|
||||||
"""Enumeration of all possible game event types."""
|
"""Enumeration of all possible game event types."""
|
||||||
COMMENT = "comment"
|
COMMENT = "comment"
|
||||||
AGENT_RESPONSE = "agent_response"
|
|
||||||
TICK = "tick"
|
TICK = "tick"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
# RPG-specific events
|
# Island survival events
|
||||||
ATTACK = "attack"
|
AGENTS_UPDATE = "agents_update" # All agents status broadcast
|
||||||
HEAL = "heal"
|
AGENT_DIED = "agent_died" # An agent has died
|
||||||
STATUS = "status"
|
FEED = "feed" # User fed an agent
|
||||||
BOSS_UPDATE = "boss_update"
|
USER_UPDATE = "user_update" # User gold/status update
|
||||||
BOSS_DEFEATED = "boss_defeated"
|
WORLD_UPDATE = "world_update" # World state update
|
||||||
PLAYER_UPDATE = "player_update"
|
CHECK = "check" # Status check response
|
||||||
|
|
||||||
|
|
||||||
class GameEvent(BaseModel):
|
class GameEvent(BaseModel):
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ fastapi>=0.109.0
|
|||||||
uvicorn[standard]>=0.27.0
|
uvicorn[standard]>=0.27.0
|
||||||
websockets>=12.0
|
websockets>=12.0
|
||||||
pydantic>=2.5.0
|
pydantic>=2.5.0
|
||||||
|
sqlalchemy>=2.0.0
|
||||||
|
aiosqlite>=0.19.0
|
||||||
|
|||||||
265
frontend/app.js
265
frontend/app.js
@@ -1,17 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* The Island - Debug Client JavaScript
|
* The Island - Survival Simulation Client
|
||||||
* Handles WebSocket connection, UI interactions, and game state display
|
* Handles WebSocket connection, agent display, and user interactions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
const WS_URL = 'ws://localhost:8080/ws';
|
const WS_URL = 'ws://localhost:8080/ws';
|
||||||
|
|
||||||
// Player state (tracked from server events)
|
// User state
|
||||||
let playerState = {
|
let userGold = 100;
|
||||||
hp: 100,
|
|
||||||
maxHp: 100,
|
// Agents state
|
||||||
gold: 0
|
let agents = [];
|
||||||
};
|
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const statusDot = document.getElementById('statusDot');
|
const statusDot = document.getElementById('statusDot');
|
||||||
@@ -22,16 +21,8 @@ const usernameInput = document.getElementById('username');
|
|||||||
const messageInput = document.getElementById('message');
|
const messageInput = document.getElementById('message');
|
||||||
const autoScrollCheckbox = document.getElementById('autoScroll');
|
const autoScrollCheckbox = document.getElementById('autoScroll');
|
||||||
const hideTicksCheckbox = document.getElementById('hideTicks');
|
const hideTicksCheckbox = document.getElementById('hideTicks');
|
||||||
|
const agentsGrid = document.getElementById('agentsGrid');
|
||||||
// Boss UI Elements
|
const userGoldDisplay = document.getElementById('userGold');
|
||||||
const bossName = document.getElementById('bossName');
|
|
||||||
const bossHpText = document.getElementById('bossHpText');
|
|
||||||
const bossHealthBar = document.getElementById('bossHealthBar');
|
|
||||||
const bossHealthLabel = document.getElementById('bossHealthLabel');
|
|
||||||
|
|
||||||
// Player UI Elements
|
|
||||||
const playerHpDisplay = document.getElementById('playerHp');
|
|
||||||
const playerGoldDisplay = document.getElementById('playerGold');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle WebSocket connection
|
* Toggle WebSocket connection
|
||||||
@@ -48,30 +39,30 @@ function toggleConnection() {
|
|||||||
* Establish WebSocket connection
|
* Establish WebSocket connection
|
||||||
*/
|
*/
|
||||||
function connect() {
|
function connect() {
|
||||||
statusText.textContent = 'Connecting...';
|
statusText.textContent = '连接中...';
|
||||||
connectBtn.disabled = true;
|
connectBtn.disabled = true;
|
||||||
|
|
||||||
ws = new WebSocket(WS_URL);
|
ws = new WebSocket(WS_URL);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
statusDot.classList.add('connected');
|
statusDot.classList.add('connected');
|
||||||
statusText.textContent = 'Connected';
|
statusText.textContent = '已连接';
|
||||||
connectBtn.textContent = 'Disconnect';
|
connectBtn.textContent = '断开';
|
||||||
connectBtn.disabled = false;
|
connectBtn.disabled = false;
|
||||||
logEvent({ event_type: 'system', data: { message: 'WebSocket connected' } });
|
logEvent({ event_type: 'system', data: { message: '已连接到荒岛服务器' } });
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
statusDot.classList.remove('connected');
|
statusDot.classList.remove('connected');
|
||||||
statusText.textContent = 'Disconnected';
|
statusText.textContent = '未连接';
|
||||||
connectBtn.textContent = 'Connect';
|
connectBtn.textContent = '连接';
|
||||||
connectBtn.disabled = false;
|
connectBtn.disabled = false;
|
||||||
logEvent({ event_type: 'system', data: { message: 'WebSocket disconnected' } });
|
logEvent({ event_type: 'system', data: { message: '与服务器断开连接' } });
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
logEvent({ event_type: 'error', data: { message: 'Connection error' } });
|
logEvent({ event_type: 'error', data: { message: '连接错误' } });
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -91,122 +82,123 @@ function handleGameEvent(event) {
|
|||||||
const eventType = event.event_type;
|
const eventType = event.event_type;
|
||||||
const data = event.data || {};
|
const data = event.data || {};
|
||||||
|
|
||||||
// Update UI based on event type
|
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'boss_update':
|
case 'agents_update':
|
||||||
updateBossUI(data);
|
updateAgentsUI(data.agents);
|
||||||
break;
|
break;
|
||||||
case 'attack':
|
case 'feed':
|
||||||
updateBossFromAttack(data);
|
case 'user_update':
|
||||||
updatePlayerFromEvent(data);
|
updateUserGold(data);
|
||||||
break;
|
break;
|
||||||
case 'heal':
|
case 'check':
|
||||||
updatePlayerFromEvent(data);
|
if (data.user && data.user.username === getCurrentUser()) {
|
||||||
break;
|
userGold = data.user.gold;
|
||||||
case 'status':
|
userGoldDisplay.textContent = userGold;
|
||||||
updatePlayerFromEvent(data);
|
|
||||||
updateBossFromStatus(data);
|
|
||||||
break;
|
|
||||||
case 'system':
|
|
||||||
// Handle player death/respawn updates
|
|
||||||
if (data.user && data.player_hp !== undefined) {
|
|
||||||
updatePlayerFromEvent(data);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'tick':
|
|
||||||
if (data.boss_hp !== undefined) {
|
|
||||||
updateBossUI({
|
|
||||||
boss_hp: data.boss_hp,
|
|
||||||
boss_max_hp: data.boss_max_hp
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the event
|
|
||||||
logEvent(event);
|
logEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update Boss health bar UI
|
* Get current username
|
||||||
*/
|
*/
|
||||||
function updateBossUI(data) {
|
function getCurrentUser() {
|
||||||
if (data.boss_name) {
|
return usernameInput.value.trim() || '观众001';
|
||||||
bossName.textContent = data.boss_name;
|
|
||||||
}
|
}
|
||||||
if (data.boss_hp !== undefined && data.boss_max_hp !== undefined) {
|
|
||||||
const hp = data.boss_hp;
|
|
||||||
const maxHp = data.boss_max_hp;
|
|
||||||
const percentage = maxHp > 0 ? (hp / maxHp) * 100 : 0;
|
|
||||||
|
|
||||||
bossHpText.textContent = `HP: ${hp} / ${maxHp}`;
|
/**
|
||||||
bossHealthBar.style.width = `${percentage}%`;
|
* Update user gold display
|
||||||
bossHealthLabel.textContent = `${Math.round(percentage)}%`;
|
*/
|
||||||
|
function updateUserGold(data) {
|
||||||
// Change color based on HP percentage
|
if (data.user === getCurrentUser() && data.gold !== undefined) {
|
||||||
if (percentage <= 25) {
|
userGold = data.gold;
|
||||||
bossHealthBar.style.background = 'linear-gradient(90deg, #ff2222 0%, #ff4444 100%)';
|
userGoldDisplay.textContent = userGold;
|
||||||
} else if (percentage <= 50) {
|
|
||||||
bossHealthBar.style.background = 'linear-gradient(90deg, #ff6600 0%, #ff8844 100%)';
|
|
||||||
} else {
|
|
||||||
bossHealthBar.style.background = 'linear-gradient(90deg, #ff4444 0%, #ff6666 100%)';
|
|
||||||
}
|
}
|
||||||
|
if (data.user_gold !== undefined && data.user === getCurrentUser()) {
|
||||||
|
userGold = data.user_gold;
|
||||||
|
userGoldDisplay.textContent = userGold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update boss from attack event
|
* Update agents UI with card view
|
||||||
*/
|
*/
|
||||||
function updateBossFromAttack(data) {
|
function updateAgentsUI(agentsData) {
|
||||||
if (data.boss_hp !== undefined && data.boss_max_hp !== undefined) {
|
if (!agentsData || agentsData.length === 0) return;
|
||||||
updateBossUI({
|
|
||||||
boss_hp: data.boss_hp,
|
agents = agentsData;
|
||||||
boss_max_hp: data.boss_max_hp
|
agentsGrid.innerHTML = '';
|
||||||
|
|
||||||
|
agents.forEach(agent => {
|
||||||
|
const card = createAgentCard(agent);
|
||||||
|
agentsGrid.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an agent card element
|
||||||
|
*/
|
||||||
|
function createAgentCard(agent) {
|
||||||
|
const isDead = agent.status !== 'Alive';
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `agent-card ${isDead ? 'dead' : ''}`;
|
||||||
|
card.id = `agent-${agent.id}`;
|
||||||
|
|
||||||
|
const statusClass = isDead ? 'dead' : 'alive';
|
||||||
|
const statusText = isDead ? '已死亡' : '存活';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="agent-header">
|
||||||
|
<div>
|
||||||
|
<span class="agent-name">${agent.name}</span>
|
||||||
|
<span class="agent-personality">${agent.personality}</span>
|
||||||
|
</div>
|
||||||
|
<span class="agent-status ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-bar-container">
|
||||||
|
<div class="stat-bar-label">
|
||||||
|
<span>❤️ 生命值</span>
|
||||||
|
<span>${agent.hp}/100</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-bar">
|
||||||
|
<div class="stat-bar-fill hp" style="width: ${agent.hp}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-bar-container">
|
||||||
|
<div class="stat-bar-label">
|
||||||
|
<span>⚡ 体力</span>
|
||||||
|
<span>${agent.energy}/100</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-bar">
|
||||||
|
<div class="stat-bar-fill energy" style="width: ${agent.energy}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="feed-btn" onclick="feedAgent('${agent.name}')" ${isDead ? 'disabled' : ''}>
|
||||||
|
🍖 投喂 (10金币)
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update boss from status event
|
* Feed an agent
|
||||||
*/
|
*/
|
||||||
function updateBossFromStatus(data) {
|
function feedAgent(agentName) {
|
||||||
if (data.boss_hp !== undefined && data.boss_max_hp !== undefined) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
updateBossUI({
|
alert('未连接到服务器');
|
||||||
boss_name: data.boss_name,
|
return;
|
||||||
boss_hp: data.boss_hp,
|
|
||||||
boss_max_hp: data.boss_max_hp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const user = getCurrentUser();
|
||||||
* Update player stats from event data
|
const payload = {
|
||||||
*/
|
action: 'send_comment',
|
||||||
function updatePlayerFromEvent(data) {
|
payload: { user, message: `feed ${agentName}` }
|
||||||
const currentUser = usernameInput.value.trim() || 'Anonymous';
|
};
|
||||||
|
|
||||||
// Only update if this event is for the current user
|
ws.send(JSON.stringify(payload));
|
||||||
if (data.user !== currentUser) return;
|
|
||||||
|
|
||||||
if (data.player_hp !== undefined) {
|
|
||||||
playerState.hp = data.player_hp;
|
|
||||||
}
|
|
||||||
if (data.player_max_hp !== undefined) {
|
|
||||||
playerState.maxHp = data.player_max_hp;
|
|
||||||
}
|
|
||||||
if (data.player_gold !== undefined) {
|
|
||||||
playerState.gold = data.player_gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update player stats UI
|
|
||||||
*/
|
|
||||||
function updatePlayerUI() {
|
|
||||||
playerHpDisplay.textContent = `${playerState.hp}/${playerState.maxHp}`;
|
|
||||||
playerGoldDisplay.textContent = playerState.gold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,15 +206,15 @@ function updatePlayerUI() {
|
|||||||
*/
|
*/
|
||||||
function sendComment() {
|
function sendComment() {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
alert('Not connected to server');
|
alert('未连接到服务器');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = usernameInput.value.trim() || 'Anonymous';
|
const user = getCurrentUser();
|
||||||
const message = messageInput.value.trim();
|
const message = messageInput.value.trim();
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
alert('Please enter a message');
|
alert('请输入指令');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,25 +227,6 @@ function sendComment() {
|
|||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Quick action buttons
|
|
||||||
*/
|
|
||||||
function quickAction(action) {
|
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
||||||
alert('Not connected to server');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = usernameInput.value.trim() || 'Anonymous';
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
action: 'send_comment',
|
|
||||||
payload: { user, message: action }
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format timestamp for display
|
* Format timestamp for display
|
||||||
*/
|
*/
|
||||||
@@ -269,20 +242,18 @@ function formatEventData(eventType, data) {
|
|||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'comment':
|
case 'comment':
|
||||||
return `${data.user}: ${data.message}`;
|
return `${data.user}: ${data.message}`;
|
||||||
case 'agent_response':
|
|
||||||
return data.response;
|
|
||||||
case 'tick':
|
case 'tick':
|
||||||
return `Tick #${data.tick} | Players: ${data.player_count || 0}`;
|
return `Tick #${data.tick} | 第${data.day}天 | 存活: ${data.alive_agents}人`;
|
||||||
case 'system':
|
case 'system':
|
||||||
case 'error':
|
case 'error':
|
||||||
|
case 'feed':
|
||||||
|
case 'agent_died':
|
||||||
|
case 'check':
|
||||||
return data.message;
|
return data.message;
|
||||||
case 'attack':
|
case 'agents_update':
|
||||||
case 'heal':
|
return `角色状态已更新`;
|
||||||
case 'status':
|
case 'user_update':
|
||||||
case 'boss_defeated':
|
return `${data.user} 金币: ${data.gold}`;
|
||||||
return data.message;
|
|
||||||
case 'boss_update':
|
|
||||||
return `Boss ${data.boss_name}: ${data.boss_hp}/${data.boss_max_hp} (${Math.round(data.boss_hp_percentage)}%)`;
|
|
||||||
default:
|
default:
|
||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
}
|
}
|
||||||
@@ -301,8 +272,8 @@ function logEvent(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip boss_update events to reduce log noise (they're reflected in the UI)
|
// Skip agents_update to reduce noise
|
||||||
if (eventType === 'boss_update') {
|
if (eventType === 'agents_update') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>The Island - Debug Client</title>
|
<title>荒岛:人性的试炼</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -12,20 +12,26 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
background: linear-gradient(135deg, #1a2a1a 0%, #0d1f0d 100%);
|
||||||
color: #eee;
|
color: #eee;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 900px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #88cc88;
|
||||||
|
text-shadow: 0 0 10px rgba(136, 204, 136, 0.5);
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: #00d4ff;
|
font-size: 0.9rem;
|
||||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
|
||||||
}
|
}
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -52,87 +58,129 @@
|
|||||||
box-shadow: 0 0 10px rgba(68, 255, 68, 0.5);
|
box-shadow: 0 0 10px rgba(68, 255, 68, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Boss Health Bar */
|
/* User Stats */
|
||||||
.boss-panel {
|
.user-panel {
|
||||||
background: rgba(255, 68, 68, 0.1);
|
background: rgba(136, 204, 136, 0.1);
|
||||||
border: 1px solid rgba(255, 68, 68, 0.3);
|
border: 1px solid rgba(136, 204, 136, 0.3);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 15px 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
|
||||||
.boss-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
.boss-name {
|
.user-info {
|
||||||
color: #ff6666;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.boss-hp-text {
|
|
||||||
color: #ffaaaa;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.health-bar-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
border-radius: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.health-bar {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #ff4444 0%, #ff6666 100%);
|
|
||||||
border-radius: 15px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
.health-bar-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-weight: bold;
|
|
||||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Player Stats Panel */
|
|
||||||
.player-panel {
|
|
||||||
background: rgba(0, 212, 255, 0.1);
|
|
||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.player-header {
|
|
||||||
color: #00d4ff;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.player-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 30px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
.stat-icon {
|
.gold-display {
|
||||||
font-size: 1.5rem;
|
font-size: 1.3rem;
|
||||||
|
color: #ffd700;
|
||||||
}
|
}
|
||||||
.stat-value {
|
|
||||||
|
/* Agent Cards */
|
||||||
|
.agents-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
color: #88cc88;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.agents-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.agent-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.agent-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.agent-card.dead {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(80%);
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
.agent-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.agent-name {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.stat-label {
|
.agent-personality {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #aaa;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.agent-status {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.agent-status.alive { background: rgba(68, 255, 68, 0.2); color: #88ff88; }
|
||||||
|
.agent-status.dead { background: rgba(255, 68, 68, 0.2); color: #ff8888; }
|
||||||
|
|
||||||
|
/* Stat Bars */
|
||||||
|
.stat-bar-container {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.stat-bar-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.stat-bar {
|
||||||
|
height: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.stat-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.stat-bar-fill.hp { background: linear-gradient(90deg, #ff4444, #ff6666); }
|
||||||
|
.stat-bar-fill.energy { background: linear-gradient(90deg, #ffaa00, #ffcc44); }
|
||||||
|
|
||||||
|
/* Feed Button */
|
||||||
|
.feed-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #88cc88;
|
||||||
|
color: #1a2a1a;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.feed-btn:hover {
|
||||||
|
background: #66aa66;
|
||||||
|
}
|
||||||
|
.feed-btn:disabled {
|
||||||
|
background: #444;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Panels */
|
||||||
.panels {
|
.panels {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -145,7 +193,7 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.panel h2 {
|
.panel h2 {
|
||||||
color: #00d4ff;
|
color: #88cc88;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
@@ -166,39 +214,26 @@
|
|||||||
}
|
}
|
||||||
input[type="text"]:focus {
|
input[type="text"]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #00d4ff;
|
border-color: #88cc88;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #00d4ff;
|
background: #88cc88;
|
||||||
color: #1a1a2e;
|
color: #1a2a1a;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #00b8e6;
|
background: #66aa66;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
button:disabled {
|
button:disabled {
|
||||||
background: #666;
|
background: #444;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.quick-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.quick-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.quick-btn.attack { background: #ff6666; }
|
|
||||||
.quick-btn.heal { background: #44ff88; }
|
|
||||||
.quick-btn.status { background: #ffaa44; }
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
@@ -212,7 +247,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.event-log {
|
.event-log {
|
||||||
height: 350px;
|
height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -226,17 +261,13 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border-left: 3px solid;
|
border-left: 3px solid;
|
||||||
}
|
}
|
||||||
.event.comment { border-color: #ffaa00; background: rgba(255, 170, 0, 0.1); }
|
.event.comment { border-color: #888; background: rgba(136, 136, 136, 0.1); }
|
||||||
.event.agent_response { border-color: #00ff88; background: rgba(0, 255, 136, 0.1); }
|
.event.tick { border-color: #444; background: rgba(68, 68, 68, 0.05); opacity: 0.6; }
|
||||||
.event.tick { border-color: #888; background: rgba(136, 136, 136, 0.05); opacity: 0.6; }
|
.event.system { border-color: #88cc88; background: rgba(136, 204, 136, 0.1); }
|
||||||
.event.system { border-color: #00d4ff; background: rgba(0, 212, 255, 0.1); }
|
|
||||||
.event.error { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
|
.event.error { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
|
||||||
/* RPG Event Styles */
|
.event.feed { border-color: #ffaa00; background: rgba(255, 170, 0, 0.1); color: #ffcc66; }
|
||||||
.event.attack { border-color: #ff4444; background: rgba(255, 68, 68, 0.15); color: #ff8888; }
|
.event.agent_died { border-color: #ff4444; background: rgba(255, 68, 68, 0.15); color: #ff8888; }
|
||||||
.event.heal { border-color: #44ff88; background: rgba(68, 255, 136, 0.15); color: #88ffaa; }
|
.event.check { border-color: #88cc88; background: rgba(136, 204, 136, 0.1); }
|
||||||
.event.status { border-color: #ffaa44; background: rgba(255, 170, 68, 0.1); }
|
|
||||||
.event.boss_update { border-color: #ff6666; background: rgba(255, 102, 102, 0.1); }
|
|
||||||
.event.boss_defeated { border-color: #ffdd00; background: rgba(255, 221, 0, 0.2); color: #ffee88; font-weight: bold; }
|
|
||||||
.event-time { color: #888; font-size: 11px; }
|
.event-time { color: #888; font-size: 11px; }
|
||||||
.event-type { font-weight: bold; text-transform: uppercase; font-size: 11px; }
|
.event-type { font-weight: bold; text-transform: uppercase; font-size: 11px; }
|
||||||
.event-data { margin-top: 5px; }
|
.event-data { margin-top: 5px; }
|
||||||
@@ -244,70 +275,57 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>The Island - Debug Client</h1>
|
<h1>荒岛:人性的试炼</h1>
|
||||||
|
<p class="subtitle">The Island: Trial of Humanity</p>
|
||||||
|
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
<div class="status-dot" id="statusDot"></div>
|
<div class="status-dot" id="statusDot"></div>
|
||||||
<span id="statusText">Disconnected</span>
|
<span id="statusText">未连接</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="connectBtn" onclick="toggleConnection()">Connect</button>
|
<button id="connectBtn" onclick="toggleConnection()">连接</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Boss Health Panel -->
|
<!-- User Panel -->
|
||||||
<div class="boss-panel">
|
<div class="user-panel">
|
||||||
<div class="boss-header">
|
<div class="user-info">
|
||||||
<span class="boss-name" id="bossName">Dragon</span>
|
<span>玩家:</span>
|
||||||
<span class="boss-hp-text" id="bossHpText">HP: 1000 / 1000</span>
|
<input type="text" id="username" value="观众001" style="width: 120px;">
|
||||||
</div>
|
</div>
|
||||||
<div class="health-bar-container">
|
<div class="gold-display">
|
||||||
<div class="health-bar" id="bossHealthBar" style="width: 100%"></div>
|
💰 <span id="userGold">100</span> 金币
|
||||||
<span class="health-bar-label" id="bossHealthLabel">100%</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Stats Panel -->
|
<!-- Agents Section -->
|
||||||
<div class="player-panel">
|
<div class="agents-section">
|
||||||
<div class="player-header">Your Stats</div>
|
<h2 class="section-title">岛上幸存者</h2>
|
||||||
<div class="player-stats">
|
<div class="agents-grid" id="agentsGrid">
|
||||||
<div class="stat-item">
|
<!-- Agent cards will be dynamically generated -->
|
||||||
<span class="stat-icon">❤️</span>
|
<div class="agent-card" id="agent-loading">
|
||||||
<div>
|
<p style="text-align: center; color: #666;">等待连接...</p>
|
||||||
<div class="stat-value" id="playerHp">100/100</div>
|
|
||||||
<div class="stat-label">HP</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-icon">💰</span>
|
|
||||||
<div>
|
|
||||||
<div class="stat-value" id="playerGold">0</div>
|
|
||||||
<div class="stat-label">Gold</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panels">
|
<div class="panels">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Send Command</h2>
|
<h2>发送指令</h2>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="username" placeholder="Username" value="TestUser">
|
<input type="text" id="message" placeholder="输入指令 (feed Jack, check, 查询...)">
|
||||||
<input type="text" id="message" placeholder="Message (attack, heal, status...)">
|
<button onclick="sendComment()">发送</button>
|
||||||
<button onclick="sendComment()">Send</button>
|
|
||||||
</div>
|
|
||||||
<div class="quick-actions">
|
|
||||||
<button class="quick-btn attack" onclick="quickAction('attack')">⚔️ Attack</button>
|
|
||||||
<button class="quick-btn heal" onclick="quickAction('heal')">💚 Heal</button>
|
|
||||||
<button class="quick-btn status" onclick="quickAction('status')">📊 Status</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p style="margin-top: 10px; font-size: 0.85rem; color: #888;">
|
||||||
|
指令: <code>feed [名字]</code> - 投喂角色 (消耗10金币) | <code>check</code> - 查询状态
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Event Log</h2>
|
<h2>事件日志</h2>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button onclick="clearLog()">Clear Log</button>
|
<button onclick="clearLog()">清空</button>
|
||||||
<label><input type="checkbox" id="autoScroll" checked> Auto-scroll</label>
|
<label><input type="checkbox" id="autoScroll" checked> 自动滚动</label>
|
||||||
<label><input type="checkbox" id="hideTicks"> Hide ticks</label>
|
<label><input type="checkbox" id="hideTicks" checked> 隐藏 Tick</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="event-log" id="eventLog"></div>
|
<div class="event-log" id="eventLog"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user