2D我的世界创造模式网页版正式出炉——《我们的2D创造世界:无限创意,多人同乐》欢迎来到ourcraft.xin网站上玩

欢迎来到《我们的2D创造世界》!这是一款基于Web技术打造的多人联机2D沙盒创造游戏,融合了经典沙盒游戏的自由创造与现代化多人联机体验。在这里,你可以与全球玩家实时互动,共同创造属于你们的独特世界。

🎮 核心玩法特色

本游戏采用直观的侧视角2D界面,操作简单却充满无限可能。通过点击鼠标即可放置或破坏方块,25种不同类型的方块任你选择------从基础材料如草地、泥土、石头,到珍贵矿物如钻石、黄金、铁矿,再到装饰性方块如玻璃、砖块、书架,每一种方块都能让你的创造力得以充分展现。

🌍 沉浸式多人体验

游戏最大的亮点在于实时多人联机功能。你可以看到其他玩家在世界的各个角落忙碌着,或建造宏伟城堡,或挖掘地下隧道,或合作搭建大型工程。内置的聊天系统让你随时与伙伴沟通策略,分享建造灵感,让创造不再孤单。

🏗️ 创造的自由度

从简单的茅草屋到复杂的地下城,从宁静的花园到壮观的天空之城,一切皆由你手。《我们的2D创造世界》提供了真正的创造自由------没有任务限制,没有等级要求,只有你和你的想象力。每天登录都会看到世界的新变化,因为其他玩家也在不断创造新的景观和建筑。

🎨 个性化体验

每位玩家都可以从六种不同颜色的皮肤中选择自己喜欢的形象,在众多玩家中脱颖而出。精心设计的用户界面让你轻松切换方块类型,实时查看在线玩家,管理自己的创造项目。

🔧 技术亮点

游戏采用现代化的Web技术栈,后端使用Flask框架确保稳定运行,前端利用Canvas技术实现流畅的2D渲染,通过WebSocket技术实现实时多人同步。数据库持久化保存所有玩家的建造成果,确保你的心血不会被遗忘。

🌟 社区驱动

除了游戏本身,我们还建立了完整的社区系统------论坛交流、个人资料展示、好友互动等功能一应俱全。你可以分享自己的建造心得,欣赏其他玩家的杰作,甚至举办建造比赛。

无论你是想独自建造一个宁静的避世之所,还是与朋友共同打造一个繁华的虚拟城市,《我们的2D创造世界》都能满足你的创造欲望。无需下载,打开浏览器即可开始你的创造之旅。世界等待着你的第一块方块!

创造无限可能,连接你我世界------我们的2D创造世界,期待你的加入!


欢迎来到ourcraft.xin网站上游玩,每一个玩家都是创造者,每一个世界都是大家的

注:本项目为开源代码,以下内容是开源的,希望大家能够点赞关注收藏


python 复制代码
import eventlet
eventlet.monkey_patch(all=True)  # 全局打补丁(必须!否则异步/协程相关功能异常)
# 2. 导入标准库和第三方基础模块
import logging
import os
from datetime import datetime
import uuid
import json
import threading
from werkzeug.utils import secure_filename  # 用于文件上传安全处理


# 3. 导入 Flask 核心及扩展模块(必须在 Eventlet 补丁后)
from flask import (
    Flask, 
    render_template, 
    request, 
    session, 
    jsonify,
    url_for, 
    send_from_directory, 
    abort, 
    redirect,
    flash  # 可选:如果需要Flash消息提示
)

from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect  # WebSocket核心库

from flask_sqlalchemy import SQLAlchemy  # ORM数据库
from flask_migrate import Migrate  # 数据库迁移工具
from flask_wtf.csrf import CSRFProtect  # CSRF保护
from flask_cors import CORS  # 跨域资源共享支持
from flask import current_app
from werkzeug.security import generate_password_hash, check_password_hash

online_players = {}  # {user_id: {'username': str, 'x': int, 'y': int, 'sid': str}}
SECRET_KEY = os.environ.get('SECRET_KEY', 'your_fixed_dev_secret_key_here')

def create_app():
    # ----------------------
    # 配置
    # ----------------------
    
    # 项目根目录
    base_dir = os.path.abspath(os.path.dirname(__file__))
    app = Flask(
        __name__,
        template_folder=os.path.join(base_dir, 'templates'),  # 绝对路径指向 templates 目录
        static_folder=os.path.join(base_dir, 'static')         # 绝对路径指向 static 目录
    )
    # 密钥(生产环境应从环境变量获取)
    app.config['SECRET_KEY'] = 'your_strong_secret_key_here'  

    # 数据库配置(SQLite)
    app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(base_dir, "app.db")}'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False  # 关闭不必要的警告

    # CSRF 保护(启用)
    app.config['WTF_CSRF_ENABLED'] = True

    # 其他通用配置
    app.config['TEMPLATES_AUTO_RELOAD'] = True  # 模板自动重载(开发环境有用)
    app.config['JSON_AS_ASCII'] = False         # JSON 不转义非 ASCII 字符
    app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 上传文件最大 16MB

    # ----------------------
    # 扩展初始化
    # ----------------------
    csrf = CSRFProtect(app)
    db = SQLAlchemy(app)
    migrate = Migrate(app, db)
    CORS(app, 
     supports_credentials=True,  # 允许跨域携带凭证
     resources={r"/*": {"origins": "http://localhost:5000"}})  # 替换为你的前端实际地址
    
    # ----------------------
    # 数据库模型
    # ----------------------
    class User(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(80), unique=True, nullable=False)
        email = db.Column(db.String(120), unique=True, nullable=False)
        password_hash = db.Column(db.String(128), nullable=False)
        register_time = db.Column(db.DateTime, default=datetime.utcnow)
        last_login_time = db.Column(db.DateTime)
        failed_login_count = db.Column(db.Integer, default=0)
        locked = db.Column(db.Boolean, default=False)
        _avatar_url = db.Column('avatar_url', db.String(200), default='default.png')
        signature = db.Column(db.Text, default="这个人很懒,还没写个性签名~")
        is_admin = db.Column(db.Boolean, default=False)
    
        @property
        def password(self):
            raise AttributeError('密码不可直接读取')
    
        @password.setter
        def password(self, password):
            self.password_hash = generate_password_hash(password)
    
        # ✅ 将 avatar_url 定义为 @property
        @property
        def avatar_url(self):
            if not self._avatar_url or self._avatar_url == 'default.png':
                return None
            # ✅ 正确:返回完整的 URL,通过 url_for
            return url_for('static', filename=f'avatars/{self._avatar_url}')
        
        # ✅ 定义 safe_avatar 作为另一个 @property
        @property
        def safe_avatar(self):
            """安全获取头像URL,确保有默认值"""
            if not self._avatar_url or self._avatar_url == 'default.png':
                return url_for('static', filename='avatars/default.png')
            return url_for('static', filename=f'avatars/{self._avatar_url}')
        
        # ✅ 为 avatar_url 设置 setter
        @avatar_url.setter
        def avatar_url(self, value):
            if not value:
                self._avatar_url = 'default.png'
            else:
                filename = os.path.basename(value)  # ✅ 提取纯文件名,如 '3ed8f47d-...png'
                self._avatar_url = filename
        def set_password(self, password):
            self.password_hash = generate_password_hash(password)
            
        def verify_password(self, password):
            return check_password_hash(self.password_hash, password)
    
    class Post(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        title = db.Column(db.String(100), nullable=False)
        content = db.Column(db.Text, nullable=False)
        time = db.Column(db.DateTime, default=datetime.utcnow)
        author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
        author = db.relationship('User', backref=db.backref('posts', lazy='dynamic'))
        likes = db.Column(db.Integer, default=0)
        views = db.Column(db.Integer, default=0)
        
    class Comment(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        content = db.Column(db.Text, nullable=False)
        time = db.Column(db.DateTime, default=datetime.utcnow)
        author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
        post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
        author = db.relationship('User', backref=db.backref('comments', lazy='dynamic'))
        post = db.relationship('Post', backref=db.backref('comments', lazy='dynamic', cascade="all, delete-orphan"))
    
    class Message(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        nickname = db.Column(db.String(50), nullable=False)
        content = db.Column(db.Text, nullable=False)
        time = db.Column(db.DateTime, default=datetime.utcnow)
    
    class Favorite(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
        post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
        time = db.Column(db.DateTime, default=datetime.utcnow)
    
        user = db.relationship('User', backref='favorites')
        post = db.relationship('Post', backref='favorited_by')
    
    class Like(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
        post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
        time = db.Column(db.DateTime, default=datetime.utcnow)
    
        user = db.relationship('User', backref='likes')
        post = db.relationship('Post', backref='post_likes')
    
    # 在数据库模型后添加
    class WorldBlock(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        x = db.Column(db.Integer, nullable=False)
        y = db.Column(db.Integer, nullable=False)
        block_type = db.Column(db.String(20), default='grass')  # 方块类型字符串
        created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
        __table_args__ = (
            db.UniqueConstraint('x', 'y', name='unique_block_position'),
        )
    
    # ---------------------- 新增:排行榜模型 ----------------------
    class Leaderboard(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(80), nullable=False)  # 玩家名
        survive_time = db.Column(db.Integer, nullable=False) # 存活时间(秒)
        reason = db.Column(db.String(200), nullable=False)   # 死亡原因
        created_at = db.Column(db.DateTime, default=datetime.utcnow) # 记录时间
    
        def __repr__(self):
            return f'<Leaderboard {self.username}>'
    
    # ==================== 重置地图函数 ====================
    def reset_world():
        """重置地图为侧视2D,上面是天,下面是地,最下面是基岩 - 修改后"""
        with app.app_context():
            try:
                # 清空现有世界数据
                WorldBlock.query.delete()
                
                # 生成新的侧视2D世界 - 修改:只生成一个连续世界
                world_width = 500  # 世界宽度
                world_height = 100  # 世界高度
                
                # 生成连续的地形,从y=60开始是地面
                for x in range(world_width):
                    for y in range(world_height):
                        # y越大表示越下方,y=0是最顶部(天空)
                        if y < 60:  # 最上面60层是空气,不存储
                            continue
                        elif y == 60:  # 第60层是草方块(地面最上面一层)
                            block_type = 'grass'
                        elif y < 70:  # 下面是泥土层
                            block_type = 'dirt'
                        elif y < 95:  # 再下面是石头层
                            block_type = 'stone'
                        else:  # 最下面5层是基岩
                            block_type = 'obsidian'
                        
                        block = WorldBlock(x=x, y=y, block_type=block_type)
                        db.session.add(block)
                
                db.session.commit()
                print(f"地图已重置为侧视2D格式:{world_width}x{world_height},连续地形")
                
                # 生成一些资源
                import random
                resources = [
                    (random.randint(20, 480), random.randint(70, 94), 'coal'),
                    (random.randint(20, 480), random.randint(70, 94), 'iron'),
                    (random.randint(20, 480), random.randint(70, 94), 'gold'),
                    (random.randint(20, 480), random.randint(70, 94), 'diamond'),
                ]
                
                for x, y, resource_type in resources:
                    block = WorldBlock.query.filter_by(x=x, y=y).first()
                    if block and block.block_type == 'stone':
                        block.block_type = resource_type
                
                # 生成一些实心树木
                for _ in range(30):  # 生成30棵树
                    tree_x = random.randint(20, 480)
                    # 确保树长在草地上
                    tree_y = 59  # 草地是y=60,树干从y=59开始(草方块上方)
                    
                    # 检查这个位置是否适合种树(下面必须是草地)
                    ground_block = WorldBlock.query.filter_by(x=tree_x, y=60).first()
                    if not ground_block or ground_block.block_type != 'grass':
                        continue
                    
                    # 树干(4-6格高)
                    trunk_height = random.randint(4, 6)
                    for i in range(trunk_height):
                        block_y = tree_y - i  # 向上生成树干
                        # 检查是否已有方块
                        existing_block = WorldBlock.query.filter_by(x=tree_x, y=block_y).first()
                        if existing_block:
                            existing_block.block_type = 'wood'
                        else:
                            block = WorldBlock(x=tree_x, y=block_y, block_type='wood')
                            db.session.add(block)
                    
                    # 实心树叶(不再空心)
                    leaf_radius = 3
                    for dx in range(-leaf_radius, leaf_radius + 1):
                        for dy in range(-leaf_radius, leaf_radius + 1):
                            # 计算树叶坐标
                            leaf_x = tree_x + dx
                            leaf_y = (tree_y - trunk_height) + dy
                            
                            # 确保不超出世界范围
                            if leaf_x < 0 or leaf_x >= world_width or leaf_y < 0 or leaf_y >= world_height:
                                continue
                            
                            
                            # 检查是否已有方块
                            existing_block = WorldBlock.query.filter_by(x=leaf_x, y=leaf_y).first()
                            if existing_block:
                                existing_block.block_type = 'leaves'
                            else:
                                block = WorldBlock(x=leaf_x, y=leaf_y, block_type='leaves')
                                db.session.add(block)
                
                # 添加一些水坑和熔岩池(确保在地面下方)
                for _ in range(8):
                    water_x = random.randint(20, 480)
                    water_y = 61  # 地面下一层
                    
                    # 创建3x3的水坑
                    for dx in range(-1, 2):
                        for dy in range(0, 3):  # 向下延伸
                            wx = water_x + dx
                            wy = water_y + dy
                            if 0 <= wx < world_width and 0 <= wy < world_height:
                                block = WorldBlock.query.filter_by(x=wx, y=wy).first()
                                if block and block.block_type in ['dirt', 'grass']:
                                    block.block_type = 'water'
                
                for _ in range(4):
                    lava_x = random.randint(20, 480)
                    lava_y = random.randint(85, 94)  # 深层
                    
                    # 创建熔岩池
                    for dx in range(-2, 3):
                        for dy in range(-1, 2):
                            lx = lava_x + dx
                            ly = lava_y + dy
                            if 0 <= lx < world_width and 0 <= ly < world_height:
                                block = WorldBlock.query.filter_by(x=lx, y=ly).first()
                                if block and block.block_type == 'stone':
                                    block.block_type = 'water'  # 暂时用water表示熔岩
                
                db.session.commit()
                print("世界生成完成,包含实心树木")
                
            except Exception as e:
                db.session.rollback()
                print(f"重置地图失败: {str(e)}")
                raise
    
    # 初始化世界(首次运行时生成基础地形)
    def init_world():
        with app.app_context():
            if WorldBlock.query.first() is None:
                reset_world()
                print("玩家初始位置:世界中央草坪上方10格 (x=250, y=50)")
    
    # ---------------------- 上下文处理器 ----------------------
    def get_current_user():
        user = None
        if 'user_id' in session:
            user = User.query.get(session['user_id'])
        return user
    
    @app.context_processor
    def inject_current_user():
        return {'current_user': get_current_user()}
    
    @app.context_processor
    def inject_models():
        return {'Comment': Comment}
    
    # ---------------------- 模板过滤器 ----------------------
    @app.template_filter('safe_avatar')
    def safe_avatar(path):
        return url_for('static', filename=f'avatars/{path or "default.png"}', _external=True)
    
    @app.template_filter('datetimeformat')
    def datetimeformat(value, format='%Y-%m-d %H:%M'):
        return value.strftime(format) if value else ''
    
    # ---------------------- 响应头设置 ----------------------
    @app.after_request
    def add_headers(response):
        response.headers['X-Frame-Options'] = 'SAMEORIGIN'
        response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS'
        return response
    
    # ---------------------- 页面路由 ----------------------
    @app.route('/')
    def index():
        user = get_current_user()
        return render_template('index.html', current_user=user)
    
    @app.route('/admin')
    def admin_page():
        return render_template('admin.html')
        
    @app.route('/minecraft.html')
    def minecraft():
        return render_template('minecraft.html')
        
    @app.route('/game.html')
    def game():
        return render_template('game.html')
    
    @app.route('/home.html')
    def home():
        user = get_current_user()
        return render_template('home.html', current_user=user)
    
    @app.route('/forum.html')
    def forum():
        user = get_current_user()
    
        # 获取当前页码,默认第1页,类型为整数
        page = request.args.get('page', 1, type=int)
        # 每页显示 5 个帖子
        per_page = 5
    
        # 按时间倒序排列,并进行分页查询
        posts_pagination = Post.query.order_by(Post.time.desc()).paginate(
            page=page, per_page=per_page, error_out=False
        )
    
        return render_template(
            'forum.html',
            current_user=user,
            posts=posts_pagination.items,      # ✅ 当前页的帖子列表(只5个)
            pagination=posts_pagination        # ✅ 传递整个分页对象,用于前端生成分页导航
        )
    
    @app.route('/messages.html')
    def messages():
        user = get_current_user()
        # 每页显示 5 条留言,获取页码参数,默认第1页
        page = request.args.get('page', 1, type=int)
        per_page = 5
    
        # 按时间倒序,分页查询留言
        messages_pagination = Message.query.order_by(Message.time.desc()).paginate(
            page=page, per_page=per_page, error_out=False
        )
    
        return render_template(
            'messages.html',
            current_user=user,
            messages=messages_pagination.items,      # 当前页的留言列表(最多5条)
            pagination=messages_pagination           # 分页对象,用于生成分页导航
        )
    
    @app.route('/news.html')
    def news():
        user = get_current_user()
        return render_template('news.html', current_user=user)
        
    @app.route('/articles.html')
    def articles():
        user = get_current_user()
        return render_template('articles.html', current_user=user)
    
    @app.route('/profile.html')
    def profile():
        user = get_current_user()
        if not user:
            return redirect(url_for('login'))
    
        # 修改计数方式
        post_count = user.posts.count() if user.posts else 0
        comment_count = user.comments.count() if user.comments else 0
        favorite_count = Favorite.query.filter_by(user_id=user.id).count()  # 修改这一行
        like_count = Like.query.filter_by(user_id=user.id).count() if user else 0
    
        # 查询该用户发布过的所有帖子(按时间倒序)
        user_posts = Post.query.filter_by(author_id=user.id).order_by(Post.time.desc()).all()
    
        # 查询该用户收藏的所有帖子(通过 Favorite 表关联 Post)
        favorite_posts = []
        if user.favorites:
            favorite_post_ids = [f.post_id for f in user.favorites]
            favorite_posts = Post.query.filter(Post.id.in_(favorite_post_ids)).order_by(Post.time.desc()).all()
    
        return render_template('profile.html',
            current_user=user,
            post_count=post_count,
            comment_count=comment_count,
            favorite_count=favorite_count,  # 使用修改后的计数
            like_count=like_count,
            user_posts=user_posts,            
            favorite_posts=favorite_posts     
        )
    
    @app.route('/user_profile.html')
    def user_profile():
        # 从 URL 参数中获取 user_id
        user_id = request.args.get('user_id')
        if not user_id:
            # 如果没有传 user_id,可以默认显示当前用户,或者跳转到首页/报错
            user = get_current_user()
            if not user:
                return redirect(url_for('login'))
            return render_template('user_profile.html', user=user)
    
        # 根据 user_id 查询数据库中的用户
        try:
            user_id = int(user_id)  # 确保是整数
        except (TypeError, ValueError):
            return render_template('404.html', error="无效的用户ID"), 404
    
        user = User.query.get(user_id)
        if not user:
            return render_template('404.html', error="用户不存在"), 404
    
        # ✅ 新增:查询该用户发布的所有帖子,并按时间倒序排列
        user_posts = Post.query.filter_by(author_id=user.id).order_by(Post.time.desc()).all()
    
        return render_template('user_profile.html', user=user, user_posts=user_posts)
    
    @app.route('/command.html')
    def command():
        user = get_current_user()
        return render_template('command.html', current_user=user)
    
    @app.route('/2d_minecraft.html')
    def two_d_minecraft():
        """2D创造游戏页面"""
        user = get_current_user()
        if not user:
            return redirect(url_for('login') + '?next=/2d_minecraft.html')
        
        # 生成CSRF token
        from flask_wtf.csrf import generate_csrf
        csrf_token = generate_csrf()
        
        return render_template('2d_minecraft.html', 
                             current_user=user, 
                             csrf_token=csrf_token)
    
    # ==================== 新增:重置地图路由 ====================
    @app.route('/api/reset_world', methods=['POST'])
    def api_reset_world():
        """API路由:重置游戏世界"""
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
            
            user = User.query.get(session['user_id'])
            if not user:
                return jsonify({'error': '用户不存在'}), 404
            
            # 检查权限:只有管理员可以重置地图
            if not user.is_admin:
                return jsonify({'error': '需要管理员权限'}), 403
            
            reset_world()
            
            return jsonify({
                'success': True,
                'message': '地图已重置为侧视2D格式'
            }), 200
            
        except Exception as e:
            return jsonify({'error': f'重置地图失败: {str(e)}'}), 500
    
    # ---------------------- API 路由 ----------------------
    #管理员模式
    @app.route('/api/admin/user/<int:user_id>', methods=['GET'])
    def get_user_info_admin(user_id):
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': '用户不存在'}), 404
    
        return jsonify({
            'id': user.id,
            'username': user.username,
            'email': user.email,
            'is_admin': user.is_admin,
            'locked': user.locked,
            'register_time': user.register_time.strftime('%Y-%m-%d %H:%M:%S') if user.register_time else None,
            'post_count': user.posts.count(),
            'comment_count': user.comments.count()
        })
    
    @app.route('/api/admin/users', methods=['GET'])
    def get_all_users():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        try:
            # 获取分页参数,默认第1页,每页10条
            page = request.args.get('page', 1, type=int)
            per_page = request.args.get('per_page', 10, type=int)
    
            # 分页查询用户
            users_pagination = User.query.order_by(User.id).paginate(
                page=page, per_page=per_page, error_out=False
            )
    
            users_data = []
            for u in users_pagination.items:
                users_data.append({
                    'id': u.id,
                    'username': u.username,
                    'email': u.email,
                    'is_admin': u.is_admin,
                    'locked': u.locked,
                    'register_time': u.register_time.strftime('%Y-%m-%d %H:%M:%S') if u.register_time else None,
                    'post_count': u.posts.count(),
                    'comment_count': u.comments.count()
                })
    
            return jsonify({
                'users': users_data,
                'pagination': {
                    'current_page': users_pagination.page,
                    'total_pages': users_pagination.pages,
                    'total_items': users_pagination.total,
                    'per_page': per_page,
                    'has_next': users_pagination.has_next,
                    'has_prev': users_pagination.has_prev
                }
            })
    
        except Exception as e:
            app.logger.error(f"获取用户列表失败: {e}")
            return jsonify({'error': '获取用户列表失败'}), 500
    
    @app.route('/api/admin/delete_user', methods=['DELETE'])
    def delete_user():
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            current_user = User.query.get(session['user_id'])
            if not current_user or not current_user.is_admin:
                return jsonify({'error': '没有管理员权限'}), 403
    
            user_id = request.args.get('user_id')
            if not user_id:
                return jsonify({'error': '用户ID不能为空'}), 400
    
            target_user = User.query.get(user_id)
            if not target_user:
                return jsonify({'error': '用户不存在'}), 404
    
            # 禁止删除自己
            if target_user.id == current_user.id:
                return jsonify({'error': '不能删除自己的账户'}, 400)
    
            # 禁止删除管理员账户
            if target_user.is_admin:
                return jsonify({'error': '不能删除管理员账户'}, 400)
    
            # 删除用户相关数据
            Post.query.filter_by(author_id=target_user.id).delete()
            Comment.query.filter_by(author_id=target_user.id).delete()
    
            db.session.delete(target_user)
            db.session.commit()
    
            return jsonify({
                'success': True,
                'message': f"用户 {target_user.username} 已删除"
            }), 200
    
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"删除用户失败: {str(e)}")
            return jsonify({'error': '操作失败,请重试'}), 500
    
    @app.route('/api/admin/posts/<int:post_id>', methods=['DELETE'])
    def admin_delete_post(post_id):
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        post = Post.query.get(post_id)
        if not post:
            return jsonify({'error': '帖子不存在'}), 404
    
        Like.query.filter_by(post_id=post_id).delete()
        Comment.query.filter_by(post_id=post_id).delete()
        Favorite.query.filter_by(post_id=post_id).delete()
    
        db.session.delete(post)
        db.session.commit()
    
        return jsonify({'success': True, 'message': f'帖子 {post_id} 已删除'})
    
    @app.route('/api/admin/comments/<int:comment_id>', methods=['DELETE'])
    def admin_delete_comment(comment_id):
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        comment = Comment.query.get(comment_id)
        if not comment:
            return jsonify({'error': '评论不存在'}), 404
    
        db.session.delete(comment)
        db.session.commit()
    
        return jsonify({'success': True, 'message': f'评论 {comment_id} 已删除'})
    
    #普通用户
    @app.route('/api/favorite/<int:post_id>', methods=['POST', 'DELETE'])
    def toggle_favorite(post_id):
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            post = Post.query.get(post_id)
            if not post:
                return jsonify({'error': '帖子不存在'}), 404
    
            user = User.query.get(session['user_id'])
            favorite = Favorite.query.filter_by(user_id=user.id, post_id=post_id).first()
    
            is_favorite = False
            if request.method == 'POST' and not favorite:
                new_favorite = Favorite(user_id=user.id, post_id=post_id)
                db.session.add(new_favorite)
                is_favorite = True
            elif request.method == 'DELETE' and favorite:
                db.session.delete(favorite)
                is_favorite = False
    
            db.session.commit()
            favorite_count = Favorite.query.filter_by(post_id=post_id).count()
    
            return jsonify({
                'success': True,
                'is_favorite': is_favorite,
                'count': favorite_count
            }), 200
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"收藏操作失败: {str(e)}")
            return jsonify({'error': '操作失败,请重试'}), 500
    
    @app.route('/api/like/<int:post_id>', methods=['POST', 'DELETE'])
    def toggle_like(post_id):
        # 必须登录才能点赞
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        user = User.query.get(session['user_id'])
        if not user:
            return jsonify({'error': '用户不存在'}), 404
    
        post = Post.query.get(post_id)
        if not post:
            return jsonify({'error': '帖子不存在'}), 404
    
        # 查找是否已经点过赞
        like = Like.query.filter_by(user_id=user.id, post_id=post_id).first()
    
        is_liked = False
    
        if request.method == 'POST' and not like:
            # 点赞:新增记录
            new_like = Like(user_id=user.id, post_id=post_id)
            db.session.add(new_like)
            is_liked = True
        elif request.method == 'DELETE' and like:
            # 取消点赞:删除记录
            db.session.delete(like)
            is_liked = False
    
        db.session.commit()
    
        # 获取该帖子的总点赞数(可选,前端可以不用)
        like_count = Like.query.filter_by(post_id=post_id).count()
    
        return jsonify({
            'success': True,
            'is_liked': is_liked,
            'like_count': like_count
        })
    
    @app.route('/api/update_password', methods=['POST'])
    def update_password():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        user = User.query.get(session['user_id'])
        if not user:
            return jsonify({'error': '用户不存在'}), 404
    
        data = request.get_json()
        if not data or 'old_password' not in data or 'new_password' not in data:
            return jsonify({'error': '旧密码和新密码不能为空'}), 400
    
        old_password = data['old_password']
        new_password = data['new_password']
    
        if len(new_password) < 6:
            return jsonify({'error': '新密码长度不能少于6位'}), 400
    
        if not user.verify_password(old_password):
            return jsonify({'error': '当前密码错误'}), 400
    
        # 设置新密码
        user.password = new_password
        db.session.commit()
    
        return jsonify({
            'success': True,
            'message': '密码修改成功,请重新登录'
        })
    
    @app.route('/api/update_profile', methods=['POST'])
    def update_profile():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        user = User.query.get(session['user_id'])
        if not user:
            return jsonify({'error': '用户不存在'}), 404
    
        data = request.get_json()
        if not data or 'signature' not in data:
            return jsonify({'error': '签名内容不能为空'}), 400
    
        new_signature = data['signature'].strip()
        if not new_signature:
            return jsonify({'error': '签名内容不能为空'}), 400
    
        user.signature = new_signature
        db.session.commit()
    
        return jsonify({
            'success': True,
            'user': {
                'id': user.id,
                'username': user.username,
                'signature': user.signature
            }
        })
    
    # 允许的文件类型
    ALLOWED_AVATAR_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
    
    def allowed_file(filename):
        return '.' in filename and \
               filename.rsplit('.', 1)[1].lower() in ALLOWED_AVATAR_EXTENSIONS
    
    @app.route('/api/upload_avatar', methods=['POST'])
    def upload_avatar():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        user = User.query.get(session['user_id'])
        if not user:
            return jsonify({'error': '用户不存在'}), 404
    
        if 'avatar' not in request.files:
            return jsonify({'error': '未选择文件'}), 400
    
        file = request.files['avatar']
        if file.filename == '':
            return jsonify({'error': '未选择文件'}), 400
    
        if not allowed_file(file.filename):
            return jsonify({'error': '不支持的文件格式,请上传 PNG、JPG、JPEG 或 GIF'}), 400
    
        filename = secure_filename(file.filename)
        # 可选:防止文件名冲突,可以使用 uuid
        unique_filename = f"{uuid.uuid4().hex}_{filename}"
        static_dir = os.path.join(base_dir, 'static')
        filepath = os.path.join(static_dir, 'avatars', unique_filename)
    
        try:
            # 确保目录存在
            os.makedirs(os.path.dirname(filepath), exist_ok=True)
            file.save(filepath)
        except Exception as e:
            app.logger.error(f"保存头像失败: {e}")
            return jsonify({'error': '头像保存失败'}), 500
    
        # 更新用户的头像URL
        user.avatar_url = unique_filename  # 直接存文件名
        db.session.commit()
    
        avatar_url = url_for('static', filename=f'avatars/{unique_filename}')
    
        return jsonify({
            'success': True,
            'avatar_url': avatar_url
        })
    
    @app.route('/api/admin/set_user_role', methods=['POST'])
    def set_user_role():
        try:
            # 权限检查:必须是管理员
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            current_user = User.query.get(session['user_id'])
            if not current_user or not current_user.is_admin:
                return jsonify({'error': '没有管理员权限'}), 403
    
            data = request.get_json() or request.form.to_dict()
            target_user_id = data.get('user_id')
            is_admin = data.get('is_admin', False)
    
            if not target_user_id:
                return jsonify({'error': '用户ID不能为空'}), 400
    
            target_user = User.query.get(target_user_id)
            if not target_user:
                return jsonify({'error': '用户不存在'}), 404
    
            # 禁止管理员取消自己的权限
            if target_user.id == current_user.id and not is_admin:
                return jsonify({'error': '不能取消自己的管理员权限'}), 400
    
            target_user.is_admin = bool(is_admin)
            db.session.commit()
    
            return jsonify({
                'success': True,
                'message': f"用户 {target_user.username} 已{'设置为' if is_admin else '取消'}管理员",
                'user': {
                    'id': target_user.id,
                    'username': target_user.username,
                    'is_admin': target_user.is_admin
                }
            }), 200
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"设置用户权限失败: {str(e)}")
            return jsonify({'error': '操作失败,请重试'}), 500
    
    @app.route('/api/admin/toggle_user_lock', methods=['POST'])
    def toggle_user_lock():
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            current_user = User.query.get(session['user_id'])
            if not current_user or not current_user.is_admin:
                return jsonify({'error': '没有管理员权限'}), 403
    
            data = request.get_json() or request.form.to_dict()
            target_user_id = data.get('user_id')
    
            if not target_user_id:
                return jsonify({'error': '用户ID不能为空'}), 400
    
            target_user = User.query.get(target_user_id)
            if not target_user:
                return jsonify({'error': '用户不存在'}), 404
    
            # 禁止锁定自己
            if target_user.id == current_user.id:
                return jsonify({'error': '不能锁定自己的账户'}, 400)
    
            # 禁止锁定任何管理员账户
            if target_user.is_admin:
                return jsonify({'error': '不能锁定管理员账户'}, 400)
    
            target_user.locked = not target_user.locked
            db.session.commit()
    
            return jsonify({
                'success': True,
                'message': f"用户 {target_user.username} 已{'锁定' if target_user.locked else '解锁'}",
                'user': {
                    'id': target_user.id,
                    'username': target_user.username,
                    'locked': target_user.locked
                }
            }), 200
    
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"锁定用户失败: {str(e)}")
            return jsonify({'error': '操作失败,请重试'}), 500
    
    # ========== 删除留言 ==========
    @app.route('/api/admin/messages/<int:message_id>', methods=['DELETE'])
    def admin_delete_message(message_id):
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        message = Message.query.get(message_id)
        if not message:
            return jsonify({'error': '留言不存在'}), 404
    
        db.session.delete(message)
        db.session.commit()
    
        return jsonify({'success': True, 'message': f'留言 {message_id} 已删除'})
    
    # ========== 获取留言列表(带分页) ==========
    @app.route('/api/admin/messages', methods=['GET'])
    def get_all_messages():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        try:
            # 获取分页参数
            page = request.args.get('page', 1, type=int)
            per_page = request.args.get('per_page', 10, type=int)
    
            # 按时间倒序,分页查询所有留言
            messages_pagination = Message.query.order_by(Message.time.desc()).paginate(
                page=page, per_page=per_page, error_out=False
            )
    
            messages_data = []
            for m in messages_pagination.items:
                messages_data.append({
                    'id': m.id,
                    'nickname': m.nickname,
                    'content': m.content[:100] + '...' if len(m.content) > 100 else m.content,  # 内容预览
                    'time': m.time.strftime('%Y-%m-%d %H:%M:%S') if m.time else None
                })
    
            return jsonify({
                'messages': messages_data,
                'pagination': {
                    'current_page': messages_pagination.page,
                    'total_pages': messages_pagination.pages,
                    'total_items': messages_pagination.total,
                    'per_page': per_page,
                    'has_next': messages_pagination.has_next,
                    'has_prev': messages_pagination.has_prev
                }
            })
    
        except Exception as e:
            app.logger.error(f"获取留言列表失败: {e}")
            return jsonify({'error': '获取留言列表失败'}), 500
    
    @app.route('/api/admin/comments', methods=['GET'])
    def get_all_comments():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        try:
            # ✅ 新增分页参数
            page = request.args.get('page', 1, type=int)
            per_page = request.args.get('per_page', 10, type=int)
    
            # ✅ 使用 paginate 进行分页查询
            comments_pagination = Comment.query.order_by(Comment.time.desc()).paginate(
                page=page, per_page=per_page, error_out=False
            )
    
            comments_data = []
            for c in comments_pagination.items:
                author = c.author
                post = c.post
                comments_data.append({
                    'id': c.id,
                    'content': c.content[:100] + '...' if len(c.content) > 100 else c.content,
                    'author_username': author.username if author else '未知',
                    'post_title': post.title if post else '未知帖子',
                    'time': c.time.strftime('%Y-%m-%d %H:%M:%S') if c.time else None,
                    'author_id': c.author_id,
                    'post_id': c.post_id
                })
    
            return jsonify({
                'comments': comments_data,
                'pagination': {
                    'current_page': comments_pagination.page,
                    'total_pages': comments_pagination.pages,
                    'total_items': comments_pagination.total,
                    'per_page': per_page,
                    'has_next': comments_pagination.has_next,
                    'has_prev': comments_pagination.has_prev
                }
            })
    
        except Exception as e:
            app.logger.error(f"获取评论列表失败: {e}")
            return jsonify({'error': '获取评论列表失败'}), 500
        
    @app.route('/api/admin/posts', methods=['GET'])
    def get_all_posts():
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
    
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '没有管理员权限'}), 403
    
        try:
            # ✅ 新增分页参数
            page = request.args.get('page', 1, type=int)
            per_page = request.args.get('per_page', 10, type=int)
    
            # ✅ 使用 paginate 进行分页查询
            posts_pagination = Post.query.order_by(Post.time.desc()).paginate(
                page=page, per_page=per_page, error_out=False
            )
    
            posts_data = []
            for p in posts_pagination.items:
                author = p.author
                posts_data.append({
                    'id': p.id,
                    'title': p.title,
                    'author_username': author.username if author else '未知',
                    'time': p.time.strftime('%Y-%m-%d %H:%M:%S') if p.time else None,
                    'views': p.views,
                    'likes': p.likes,
                    'comment_count': len(p.comments.all()),  # 评论数
                    'author_id': p.author_id
                })
    
            return jsonify({
                'posts': posts_data,
                'pagination': {
                    'current_page': posts_pagination.page,
                    'total_pages': posts_pagination.pages,
                    'total_items': posts_pagination.total,
                    'per_page': per_page,
                    'has_next': posts_pagination.has_next,
                    'has_prev': posts_pagination.has_prev
                }
            })
    
        except Exception as e:
            app.logger.error(f"获取帖子列表失败: {e}")
            return jsonify({'error': '获取帖子列表失败'}), 500
    
    @app.route('/post/<int:post_id>')
    def post_detail(post_id):
        current_user = get_current_user()
        post = Post.query.get_or_404(post_id)
        post.views = (post.views or 0) + 1
        db.session.commit()
        comments = Comment.query.filter_by(post_id=post_id).order_by(Comment.time.desc()).all()
        author = post.author
        author_posts_count = author.posts.count() if author else 0
        author_comments_count = author.comments.count() if author else 0
    
        is_favorite = False
        if current_user:
            is_favorite = Favorite.query.filter_by(
                user_id=current_user.id, post_id=post_id
            ).first() is not None
    
        is_liked = False
        if current_user:
            is_liked = Like.query.filter_by(
                user_id=current_user.id, post_id=post_id
            ).first() is not None
    
        # ✅ 新增:计算该帖子总共被点赞多少次
        like_count = Like.query.filter_by(post_id=post_id).count()
    
        return render_template(
            'post.html',
            post=post,
            comments=comments,
            current_user=current_user,
            is_favorite=is_favorite,
            is_liked=is_liked,
            author_posts_count=author_posts_count,
            author_comments_count=author_comments_count,
            like_count=like_count  # ✅ 传入模板
        )
    
    # 其他原有API(注册、登录、发帖等)与原始代码完全一致...
    @app.route('/register', methods=['POST'])
    @csrf.exempt
    def register():
        try:
            data = request.get_json() or request.form.to_dict()
            if not data:
                return jsonify({'error': '请提交数据'}), 400
    
            username = data.get('username')
            email = data.get('email')
            password = data.get('password')
    
            if not all([username, email, password]):
                return jsonify({'error': '用户名、邮箱、密码不能为空'}), 400
    
            if User.query.filter_by(username=username).first():
                return jsonify({'error': '用户名已存在'}), 400
    
            new_user = User(username=username, email=email)
            new_user.password = password
            db.session.add(new_user)
            db.session.commit()
            session['user_id'] = new_user.id
    
            return jsonify({
                'success': True,
                'message': '注册成功',
                'redirect': '/index.html',
                'user': {
                    'id': new_user.id,
                    'username': new_user.username,
                    'avatar_url': new_user.avatar_url,
                    'signature': new_user.signature
                }
            }), 201
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"注册失败: {str(e)}")
            return jsonify({'error': f'注册失败:{str(e)}'}), 500
    
    # 原登录路由处理函数(部分代码)
    @app.route('/login', methods=['POST'])
    @csrf.exempt
    def login():
        try:
            data = request.get_json() or request.form.to_dict()
            if not data:
                return jsonify({'error': '请提交数据'}), 400
    
            username = data.get('username')
            password = data.get('password')
            user = User.query.filter_by(username=username).first()
    
            # 保留管理员手动锁定的检查(关键)
            if user and user.locked:
                return jsonify({'error': '账户已被管理员锁定'}), 403
    
            # 统一错误提示(安全优化:避免暴露用户是否存在)
            if not user or not user.verify_password(password):
                return jsonify({'error': '用户不存在或密码错误'}), 401
    
            # 登录成功处理(新增:更新最后登录时间)
            user.last_login_time = datetime.utcnow()  # ✅ 关键修改
            db.session.commit()  # ✅ 提交数据库更改
    
            session['user_id'] = user.id
    
            next_page = request.args.get('next', 'index.html')
            return jsonify({
                'success': True,
                'message': '登录成功',
                'redirect': next_page,
                'user': {
                    'id': user.id,
                    'username': user.username,
                    'avatar_url': user.avatar_url,
                    'signature': user.signature,
                    'last_login_time': user.last_login_time.strftime('%Y-%m-%d %H:%M:%S')  # 可选:返回给前端
                }
            }), 200
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"登录失败: {str(e)}")
            return jsonify({'error': f'登录失败:{str(e)}'}), 500
    
    @app.route('/logout', methods=['GET', 'POST'])  # 允许 GET 和 POST
    def logout():
        try:
            session.pop('user_id', None)
            return jsonify({
                'success': True,
                'message': '已成功登出'
            }), 200
        except Exception as e:
            app.logger.error(f"登出失败: {str(e)}")
            return jsonify({'error': '登出失败'}), 500
    
    @app.route('/forum/posts', methods=['POST'])
    def create_post():
        try:
            app.logger.debug(f"收到发帖请求: {request.method} {request.path}")
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            data = request.get_json() or request.form.to_dict()
            if not data.get('title') or not data.get('content'):
                return jsonify({'error': '标题和内容不能为空'}), 400
    
            user = User.query.get(session['user_id'])
            new_post = Post(
                title=data['title'],
                content=data['content'],
                author_id=user.id
            )
            db.session.add(new_post)
            db.session.commit()
    
            return jsonify({
                'success': True,
                'message': '帖子发布成功',
                'post_id': new_post.id
            }), 201
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"发布帖子失败: {str(e)}", exc_info=True)
            return jsonify({'error': '发布失败,请重试'}), 500
    
    @app.route('/forum/posts/<int:post_id>', methods=['DELETE'])
    def delete_post(post_id):
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            current_user = User.query.get(session['user_id'])
            if not current_user:
                return jsonify({'error': '用户不存在'}), 404
    
            post = Post.query.get(post_id)
            if not post:
                return jsonify({'error': '帖子不存在'}), 404
    
            if post.author_id != current_user.id:
                return jsonify({'error': '您没有权限删除此帖子'}), 403
    
            # ✅ 先删除所有关联的点赞记录(避免 NOT NULL 错误)
            Like.query.filter_by(post_id=post_id).delete()
    
            # ✅ 再删除关联的评论和收藏
            Comment.query.filter_by(post_id=post_id).delete()
            Favorite.query.filter_by(post_id=post_id).delete()
    
            # ✅ 最后删除帖子本身
            db.session.delete(post)
            db.session.commit()
    
            return jsonify({'success': True, 'message': '帖子已删除'})
    
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"删除帖子失败: {str(e)}")
            return jsonify({'error': '删除失败,请重试'}), 500
            
    @app.route('/forum/posts/<int:post_id>/comments', methods=['POST'])
    def create_comment(post_id):
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            data = request.get_json() or request.form.to_dict()
            content = data.get('content', '').strip()
            if not content:
                return jsonify({'error': '评论内容不能为空'}), 400
    
            post = Post.query.get(post_id)
            if not post:
                return jsonify({'error': '帖子不存在'}), 404
    
            user = User.query.get(session['user_id'])
            new_comment = Comment(
                content=content,
                author_id=user.id,
                post_id=post_id
            )
            db.session.add(new_comment)
            db.session.commit()
    
            return jsonify({
                'success': True,
                'message': '评论发布成功',
                'comment': {
                    'id': new_comment.id,
                    'content': new_comment.content,
                    'user_id': user.id,
                    'user_name': user.username,
                    'user_avatar': user.avatar_url,
                    'create_time': new_comment.time.strftime('%Y-%m-%d %H:%M:%S')
                }
            }), 201
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"发布评论失败: {str(e)}")
            return jsonify({'error': '评论失败,请重试'}), 500
    
    @app.route('/forum/comments/<int:comment_id>', methods=['DELETE'])
    def delete_comment(comment_id):
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
    
            comment = Comment.query.get(comment_id)
            if not comment:
                return jsonify({'error': '评论不存在'}), 404
    
            current_user = User.query.get(session['user_id'])
            # 管理员可删除任意评论
            if comment.author_id != current_user.id and not current_user.is_admin:
                return jsonify({'error': '没有权限删除此评论'}), 403
    
            db.session.delete(comment)
            db.session.commit()
    
            return jsonify({
                'success': True,
                'message': '评论已删除'
            }), 200
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"删除评论失败: {str(e)}")
            return jsonify({'error': '删除评论失败,请重试'}), 500
    
    # ==================== 修改:获取世界方块数据API ====================
    @app.route('/api/world_blocks', methods=['GET'])
    def get_world_blocks():
        """获取世界方块数据"""
        try:
            print("正在获取世界方块数据...")
            
            # 从数据库查询所有方块数据
            blocks = WorldBlock.query.all()
            print(f"查询到 {len(blocks)} 个方块")
            
            # 将数据转换为前端需要的格式
            world_data = {}
            block_type_map = {
                'air': 0, 'grass': 1, 'dirt': 2, 'stone': 3,
                'wood': 4, 'leaves': 5, 'water': 6, 'sand': 7,
                'glass': 8, 'brick': 9, 'snow': 10, 'obsidian': 11,
                'diamond': 12, 'gold': 13, 'iron': 14, 'coal': 15
            }
            
            for block in blocks:
                # 使用chunk系统组织数据
                chunk_x = block.x // 16
                chunk_y = block.y // 16
                block_x = block.x % 16
                block_y = block.y % 16
                
                chunk_key = f"{chunk_x},{chunk_y}"
                block_key = f"{block_x},{block_y}"
                
                if chunk_key not in world_data:
                    world_data[chunk_key] = {}
                
                # 获取方块数字ID
                block_id = block_type_map.get(block.block_type, 0)
                world_data[chunk_key][block_key] = block_id
            
            print(f"转换完成,共 {len(world_data)} 个区块")
            return jsonify({
                'success': True,
                'world_data': world_data
            }), 200
            
        except Exception as e:
            import traceback
            app.logger.error(f"获取世界数据失败: {str(e)}\n{traceback.format_exc()}")
            return jsonify({'error': f'获取世界数据失败: {str(e)}'}), 500
    
    @app.route('/api/update_block', methods=['POST'])
    def update_block():
        """更新方块数据"""
        try:
            if 'user_id' not in session:
                return jsonify({'error': '请先登录'}), 401
            
            data = request.get_json()
            if not data:
                return jsonify({'error': '数据不能为空'}), 400
            
            x = data.get('x')
            y = data.get('y')
            block_type = data.get('blockType')  # 这是数字ID
            
            if None in [x, y, block_type]:
                return jsonify({'error': '缺少必要参数'}), 400
            
            # 将数字ID转换为字符串类型
            block_type_map = {
                0: 'air', 1: 'grass', 2: 'dirt', 3: 'stone',
                4: 'wood', 5: 'leaves', 6: 'water', 7: 'sand',
                8: 'glass', 9: 'brick', 10: 'snow', 11: 'obsidian',
                12: 'diamond', 13: 'gold', 14: 'iron', 15: 'coal'
            }
            
            block_type_str = block_type_map.get(block_type, 'air')
            
            # 查找现有的方块记录
            block = WorldBlock.query.filter_by(x=x, y=y).first()
            
            if block_type_str == 'air':  # 如果是空气,则删除方块记录
                if block:
                    db.session.delete(block)
            else:  # 更新或创建方块记录
                if block:
                    block.block_type = block_type_str
                else:
                    block = WorldBlock(x=x, y=y, block_type=block_type_str)
                    db.session.add(block)
            
            db.session.commit()
            
            return jsonify({
                'success': True,
                'message': '方块更新成功'
            }), 200
            
        except Exception as e:
            db.session.rollback()
            import traceback
            app.logger.error(f"更新方块失败: {str(e)}\n{traceback.format_exc()}")
            return jsonify({'error': '更新方块失败'}), 500
        
    # ---------------------- 新增:排行榜API ----------------------
    @app.route('/api/leaderboard', methods=['GET'])
    def get_leaderboard():
        """获取游戏排行榜数据"""
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
        
        current_user = User.query.get(session['user_id'])
        if not current_user or not current_user.is_admin:
            return jsonify({'error': '无权限访问排行榜'}), 403
        
        # 从数据库查询排行榜数据(示例:按存活时间排序)
        leaderboard = Leaderboard.query.order_by(Leaderboard.survive_time.desc()).limit(10).all()
        
        leaderboard_data = [{
            'rank': idx+1,
            'username': item.username,
            'survive_time': item.survive_time,
            'reason': item.reason
        } for idx, item in enumerate(leaderboard)]
        
        return jsonify(leaderboard_data)
    
    
    @app.route('/api/save-leaderboard', methods=['POST'])
    def save_leaderboard():
        """保存游戏结果到排行榜"""
        if 'user_id' not in session:
            return jsonify({'error': '请先登录'}), 401
        
        data = request.get_json()
        if not data:
            return jsonify({'error': '数据不能为空'}), 400
        
        # 验证数据完整性
        required_fields = ['name', 'time', 'reason']
        if not all(field in data for field in required_fields):
            return jsonify({'error': '缺少必要字段'}), 400
        
        # 创建排行榜记录
        new_record = Leaderboard(
            username=data['name'],
            survive_time=data['time'],
            reason=data['reason']
        )
        db.session.add(new_record)
        db.session.commit()
        
        return jsonify({'success': True, 'message': '成绩已保存到排行榜'}), 201
        
    @app.route('/api/userinfo')
    def get_user_info():
        try:
            if 'user_id' not in session:
                return jsonify({'logged_in': False}), 401
    
            user = User.query.get(session['user_id'])
            if not user:
                session.pop('user_id', None)
                return jsonify({'logged_in': False}), 401
    
            return jsonify({
                'logged_in': True,
                'username': user.username,
                'avatar_url': user.avatar_url,
                'register_time': user.register_time.strftime('%Y-%m-%d %H:%M:%S'),
                'post_count': user.posts.count(),
                'comment_count': user.comments.count(),
                'signature': user.signature,
                'is_admin': user.is_admin
            })
        except Exception as e:
            app.logger.error(f"获取用户信息失败: {str(e)}")
            return jsonify({'error': '获取用户信息失败', 'logged_in': False}), 500
    
    @app.route('/process_message', methods=['POST'])
    def process_message():
        try:
            # 1. 获取表单数据
            data = request.form.to_dict()
            nickname = data.get('nickname').strip()
            message_content = data.get('message').strip()
            captcha = data.get('captcha').strip()
            real_captcha = data.get('captcha_real')
    
            # 2. 校验验证码
            if captcha.upper() != real_captcha.upper():
                return jsonify({
                    'success': False,
                    'error': '验证码错误'
                }), 400
    
            # 3. 校验昵称和留言内容
            if not nickname or len(nickname) > 20:
                return jsonify({
                    'success': False,
                    'error': '昵称必填且不超过20字符'
                }), 400
    
            if not message_content or len(message_content) > 500:
                return jsonify({
                    'success': False,
                    'error': '留言内容必填且不超过500字符'
                }), 400
    
            # 4. 创建并保存留言
            new_message = Message(
                nickname=nickname,
                content=message_content,
                time=datetime.utcnow()
            )
            db.session.add(new_message)
            db.session.commit()
    
            # 5. 返回成功响应
            return jsonify({
                'success': True,
                'message': '留言提交成功'
            }), 201
    
        except Exception as e:
            db.session.rollback()
            app.logger.error(f"提交留言失败: {str(e)}")
            return jsonify({
                'success': False,
                'error': '服务器内部错误'
            }), 500
    
    # ---------------------- SocketIO 事件处理 ----------------------
    @app.route('/socket.io/')
    def socketio_test():
        return "Socket.IO endpoint"
    
    # ---------------------- 错误处理器 ----------------------
    @app.errorhandler(404)
    def page_not_found(e):
        return render_template('404.html', error=str(e)), 404
    
    @app.errorhandler(500)
    def internal_server_error(e):
        app.logger.error(f"服务器内部错误: {str(e)}")
        return render_template('500.html', error="服务器内部错误,请稍后重试"), 500

    return app, db, migrate

# ---------------------- 主程序入口 ----------------------
# 创建一个全局变量来存储app实例
app, db, migrate = create_app()

# 初始化SocketIO
socketio = SocketIO(
    app, 
    async_mode='eventlet', 
    cors_allowed_origins="*", 
    ping_timeout=60, 
    ping_interval=25,
    logger=True,
    engineio_logger=True
)

# ==================== 游戏状态存储 ====================
online_players = {}  # {username: {data}}
world_lock = threading.Lock()  # 线程锁

# SocketIO事件处理
@socketio.on('connect')
def handle_connect():
    """处理客户端连接"""
    print(f"Client connected: {request.sid}")
    emit('connected', {'status': 'connected', 'sid': request.sid})

@socketio.on('disconnect')
def handle_disconnect():
    """处理客户端断开连接"""
    print(f"Client disconnected: {request.sid}")
    
    # 从在线玩家中移除
    username_to_remove = None
    for username, player_data in online_players.items():
        if player_data.get('sid') == request.sid:
            username_to_remove = username
            break
    
    if username_to_remove:
        del online_players[username_to_remove]
        # 广播玩家离开
        socketio.emit('player_left', {
            'username': username_to_remove
        })
        
        # 更新其他玩家的在线列表
        socketio.emit('players_list', {
            'players': list(online_players.values())
        })

@socketio.on('player_join')
def handle_player_join(data):
    """处理玩家加入游戏"""
    try:
        username = data.get('username')
        if not username:
            return
        
        print(f"玩家加入: {username}")
        
        # 检查玩家是否已经在线
        if username in online_players:
            # 更新连接信息
            online_players[username]['sid'] = request.sid
        else:
            # 添加新玩家
            online_players[username] = {
                'username': username,
                'x': data.get('x', 100),
                'y': data.get('y', 200),
                'color': data.get('color', '#3d85c6'),
                'selectedBlock': data.get('selectedBlock', 1),
                'sid': request.sid
            }
        
        # 广播玩家加入
        socketio.emit('player_joined', {
            'username': username,
            'x': data.get('x', 100),
            'y': data.get('y', 200),
            'color': data.get('color', '#3d85c6')
        })
        
        # 发送当前在线玩家列表
        socketio.emit('players_list', {
            'players': list(online_players.values())
        }, room=request.sid)
        
        print(f"当前在线玩家: {list(online_players.keys())}")
        
    except Exception as e:
        print(f"Error in player_join: {str(e)}")
        import traceback
        traceback.print_exc()

@socketio.on('player_update')
def handle_player_update(data):
    """处理玩家位置更新"""
    try:
        username = None
        # 查找这个sid对应的用户名
        for uname, player_data in online_players.items():
            if player_data.get('sid') == request.sid:
                username = uname
                break
        
        if not username:
            return
        
        # 更新玩家位置
        online_players[username]['x'] = data.get('x', 0)
        online_players[username]['y'] = data.get('y', 0)
        
        # 广播给其他玩家
        socketio.emit('player_updated', {
            'username': username,
            'x': data.get('x', 0),
            'y': data.get('y', 0)
        }, skip_sid=request.sid)
        
    except Exception as e:
        print(f"Error in player_update: {str(e)}")

@socketio.on('block_update')
def handle_block_update(data):
    """处理方块更新 - 现在只用于实时同步"""
    try:
        x = data.get('x')
        y = data.get('y')
        block_type = data.get('blockType')
        
        if None in [x, y, block_type]:
            return
        
        # 广播方块更新给所有玩家
        socketio.emit('block_updated', {
            'x': x,
            'y': y,
            'blockType': block_type
        })
        
        print(f"方块更新广播: ({x}, {y}) -> {block_type}")
        
    except Exception as e:
        print(f"Error in block_update: {str(e)}")

@socketio.on('chat_message')
def handle_chat_message(data):
    """处理聊天消息"""
    try:
        message = data.get('message', '').strip()
        sender = data.get('sender', '')
        
        if not message or not sender:
            return
        
        # 广播聊天消息
        socketio.emit('chat_message', {
            'sender': sender,
            'message': message
        })
        
        print(f"聊天消息: {sender}: {message}")
        
    except Exception as e:
        print(f"Error in chat_message: {str(e)}")


if __name__ == '__main__':
    # 首先确保数据库和表已创建
    with app.app_context():
        # 确保数据库目录存在
        db_path = app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '')
        db_dir = os.path.dirname(db_path)
        if db_dir and not os.path.exists(db_dir):
            os.makedirs(db_dir, exist_ok=True)
        
        # 创建所有表
        db.create_all()
        
        # 创建默认管理员账户(如果不存在)
        # ✅ 注意:现在在app上下文中,可以直接访问User类
        if not db.session.query(User).filter_by(is_admin=True).first():
            admin = User(
                username='admin',
                email='admin@example.com',
                password_hash=generate_password_hash('ourcraftadminyyds'),
                is_admin=True,
                register_time=datetime.utcnow()
            )
            db.session.add(admin)
            db.session.commit()
            print("默认管理员账户已创建: admin / ourcraftadminyyds")
        
        # 初始化世界
        from app import reset_world  # ✅ 引入重置函数
        if WorldBlock.query.first() is None:
            reset_world()
    
    # 启动应用
    print("=" * 50)
    print("启动服务器...")
    print("访问地址: http://localhost:5000")
    print("2D创造游戏: http://localhost:5000/2d_minecraft.html")
    print("=" * 50)
    
    socketio.run(app, debug=True, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True)
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2D创造世界 - 多人联机版</title>
    <!-- CSRF Token Meta -->
    <meta name="csrf-token" content="{{ csrf_token }}">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            color: white;
            height: 100vh;
            overflow: hidden;
        }
        
        #game-container {
            position: relative;
            width: 100%;
            height: 100%;
            display: none;
        }
        
        #game-canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(to bottom, #7ec0ee 0%, #87ceeb 50%, #7ec0ee 100%);
            cursor: crosshair;
        }
        
        #login-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1000;
        }
        
        #login-form {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            padding: 40px;
            border-radius: 15px;
            width: 90%;
            max-width: 400px;
            border: 1px solid rgba(255, 255, 255, 0.2);
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
            text-align: center;
        }
        
        .form-group { margin-bottom: 20px; text-align: left; }
        .form-group label { display: block; margin-bottom: 8px; color: #ccc; }
        .form-group input {
            width: 100%;
            padding: 12px 15px;
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 8px;
            color: white;
            font-size: 16px;
        }
        
        #login-btn {
            width: 100%;
            padding: 14px;
            background: linear-gradient(135deg, #4e54c8 0%, #8f94fb 100%);
            border: none;
            border-radius: 8px;
            color: white;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        
        #login-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(78, 84, 200, 0.4);
        }
        
        #login-message {
            margin-top: 15px;
            color: #ff6b6b;
            font-size: 14px;
            min-height: 20px;
        }
        
        #ui-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 100;
        }
        
        #game-info {
            position: absolute;
            top: 20px;
            left: 20px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 10px 15px;
            border-radius: 8px;
            z-index: 101;
            display: flex;
            flex-direction: column;
            gap: 5px;
            font-family: 'Courier New', monospace;
            border: 1px solid rgba(255, 255, 255, 0.1);
            pointer-events: auto;
        }
        
        .info-item { display: flex; align-items: center; gap: 10px; }
        .info-label { font-size: 12px; opacity: 0.8; min-width: 70px; }
        .info-value { font-size: 14px; font-weight: bold; }
        
        #block-selector {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.8);
            padding: 10px 20px;
            border-radius: 10px;
            z-index: 101;
            display: flex;
            gap: 10px;
            border: 2px solid rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            pointer-events: auto;
        }
        
        .block-option {
            width: 40px;
            height: 40px;
            border: 2px solid transparent;
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.2s;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 12px;
            pointer-events: auto;
        }
        
        .block-option.selected {
            border-color: #4e54c8;
            box-shadow: 0 0 10px rgba(78, 84, 200, 0.5);
            transform: translateY(-3px);
        }
        
        /* 修改:聊天框始终显示 */
        #chat-box {
            position: absolute;
            bottom: 80px;
            right: 20px;
            width: 300px;
            height: 350px;
            background: rgba(0, 0, 0, 0.8);
            border-radius: 10px;
            z-index: 101;
            display: flex;
            flex-direction: column;
            border: 1px solid rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            pointer-events: auto;
            /* 修改:移除display: none */
        }
        
        #chat-header {
            padding: 10px 15px;
            background: rgba(255, 255, 255, 0.1);
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            font-weight: bold;
            font-size: 14px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-radius: 10px 10px 0 0;
        }
        
        #chat-messages {
            flex: 1;
            padding: 10px;
            overflow-y: auto;
            max-height: 250px;
            min-height: 250px;
        }
        
        .chat-message {
            margin-bottom: 5px;
            padding: 5px 8px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 4px;
            font-size: 12px;
            word-wrap: break-word;
        }
        
        #chat-input {
            padding: 10px;
            background: rgba(255, 255, 255, 0.1);
            border: none;
            border-top: 1px solid rgba(255, 255, 255, 0.1);
            color: white;
            font-size: 14px;
            border-radius: 0 0 10px 10px;
            pointer-events: auto;
        }
        
        #players-list {
            position: absolute;
            top: 20px;
            right: 340px; /* 给聊天框留出空间 */
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 10px 15px;
            border-radius: 8px;
            z-index: 101;
            display: flex;
            flex-direction: column;
            gap: 5px;
            font-family: 'Courier New', monospace;
            border: 1px solid rgba(255, 255, 255, 0.1);
            min-width: 150px;
            pointer-events: auto;
        }
        
        .player-item {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 5px;
            border-radius: 4px;
        }
        
        .player-color {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            display: inline-block;
        }
        
        .control-btn {
            position: absolute;
            z-index: 101;
            padding: 8px 16px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            transition: all 0.2s;
            pointer-events: auto;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        #reset-world-btn {
            top: 20px;
            left: 200px;
        }
        
        /* 移除聊天按钮,因为聊天框始终显示 */
        #chat-btn {
            display: none;
        }
        
        #loading-screen {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 2000;
            flex-direction: column;
            gap: 20px;
            display: none;
        }
        
        .loading-spinner {
            width: 50px;
            height: 50px;
            border: 5px solid rgba(255, 255, 255, 0.1);
            border-top: 5px solid #4e54c8;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        /* 皮肤选择器 */
        #skin-selector {
            position: absolute;
            bottom: 80px;
            left: 20px;
            background: rgba(0, 0, 0, 0.8);
            padding: 10px;
            border-radius: 10px;
            z-index: 101;
            display: flex;
            flex-direction: column;
            gap: 8px;
            border: 1px solid rgba(255, 255, 255, 0.1);
            pointer-events: auto;
        }
        
        .skin-option {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid transparent;
            transition: all 0.2s;
        }
        
        .skin-option.selected {
            border-color: white;
            transform: scale(1.2);
        }
    </style>
</head>
<body>
    <!-- 登录界面 -->
    <div id="login-overlay">
        <div id="login-form">
            <h2>2D创造世界</h2>
            <div class="form-group">
                <label for="username">用户名</label>
                <input type="text" id="username" placeholder="请输入用户名" required>
            </div>
            <div class="form-group">
                <label for="password">密码</label>
                <input type="password" id="password" placeholder="请输入密码" required>
            </div>
            <button id="login-btn">进入游戏</button>
            <div id="login-message"></div>
        </div>
    </div>
    
    <!-- 加载界面 -->
    <div id="loading-screen">
        <div class="loading-spinner"></div>
        <div id="loading-text">正在连接服务器...</div>
    </div>
    
    <!-- 游戏界面 -->
    <div id="game-container">
        <canvas id="game-canvas"></canvas>
        
        <div id="ui-overlay">
            <!-- 游戏信息 -->
            <div id="game-info">
                <div class="info-item">
                    <div class="info-label">玩家:</div>
                    <div id="player-name" class="info-value">未登录</div>
                </div>
                <div class="info-item">
                    <div class="info-label">位置:</div>
                    <div id="player-pos" class="info-value">0, 0</div>
                </div>
                <div class="info-item">
                    <div class="info-label">在线:</div>
                    <div id="online-count" class="info-value">1</div>
                </div>
            </div>
            
            <!-- 玩家列表 -->
            <div id="players-list">
                <div style="font-weight: bold; margin-bottom: 10px;">在线玩家</div>
            </div>
            
            <!-- 皮肤选择器 -->
            <div id="skin-selector">
                <div style="font-size: 10px; margin-bottom: 5px; opacity: 0.8;">选择皮肤</div>
                <div style="display: flex; gap: 8px;">
                    <div class="skin-option" style="background: #3d85c6;" data-color="#3d85c6"></div>
                    <div class="skin-option" style="background: #e06666;" data-color="#e06666"></div>
                    <div class="skin-option" style="background: #93c47d;" data-color="#93c47d"></div>
                    <div class="skin-option" style="background: #ffd966;" data-color="#ffd966"></div>
                    <div class="skin-option" style="background: #6fa8dc;" data-color="#6fa8dc"></div>
                    <div class="skin-option" style="background: #8e7cc3;" data-color="#8e7cc3"></div>
                </div>
            </div>
            
            <!-- 方块选择器 -->
            <div id="block-selector"></div>
            
            <!-- 聊天框(始终显示) -->
            <div id="chat-box">
                <div id="chat-header">
                    <span>聊天</span>
                    <span style="font-size: 10px; opacity: 0.7;">按T键聚焦</span>
                </div>
                <div id="chat-messages"></div>
                <input type="text" id="chat-input" placeholder="输入消息 (按Enter发送)">
            </div>
            
            <!-- 控制按钮 -->
            <button id="reset-world-btn" class="control-btn">重置世界</button>
        </div>
    </div>

    <!-- Socket.IO 客户端库 -->
    <script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
    
    <script>
        // ==================== 游戏核心配置 ====================
        const canvas = document.getElementById('game-canvas');
        const ctx = canvas.getContext('2d');
        
        let CANVAS_WIDTH = 1200;
        let CANVAS_HEIGHT = 700;
        canvas.width = CANVAS_WIDTH;
        canvas.height = CANVAS_HEIGHT;
        
        // 游戏常量
        const BLOCK_SIZE = 32;
        const CHUNK_SIZE = 16;
        const PLAYER_SPEED = 5;
        const PLAYER_WIDTH = 24;
        const PLAYER_HEIGHT = 36;
        const PLAYER_INTERACTION_RANGE = 5 * BLOCK_SIZE;
        
        // 扩展方块类型定义(增加更多方块)
        const BLOCK_TYPES = {
            0: { id: 0, name: "空气", color: "transparent", solid: false },
            1: { id: 1, name: "草地", color: "#5a9c5a", solid: true },
            2: { id: 2, name: "泥土", color: "#8B4513", solid: true },
            3: { id: 3, name: "石头", color: "#808080", solid: true },
            4: { id: 4, name: "木材", color: "#4d2600", solid: true },
            5: { id: 5, name: "树叶", color: "#228B22", solid: true },
            6: { id: 6, name: "水", color: "#1E90FF", solid: false },
            7: { id: 7, name: "沙子", color: "#C2B280", solid: true },
            8: { id: 8, name: "玻璃", color: "#87CEEB", solid: true, transparent: true },
            9: { id: 9, name: "砖块", color: "#B22222", solid: true },
            10: { id: 10, name: "雪地", color: "#F0F8FF", solid: true },
            11: { id: 11, name: "基岩", color: "#191970", solid: true },
            12: { id: 12, name: "钻石矿", color: "#00FFFF", solid: true },
            13: { id: 13, name: "金矿", color: "#FFD700", solid: true },
            14: { id: 14, name: "铁矿", color: "#D3D3D3", solid: true },
            15: { id: 15, name: "煤矿", color: "#2F4F4F", solid: true },
            16: { id: 16, name: "红石", color: "#FF0000", solid: true },
            17: { id: 17, name: "青金石", color: "#4169E1", solid: true },
            18: { id: 18, name: "绿宝石", color: "#32CD32", solid: true },
            19: { id: 19, name: "下界岩", color: "#8B0000", solid: true },
            20: { id: 20, name: "灵魂沙", color: "#8B7355", solid: true },
            21: { id: 21, name: "萤石", color: "#FFFF99", solid: true },
            22: { id: 22, name: "粘土", color: "#8B7355", solid: true },
            23: { id: 23, name: "冰", color: "#87CEEB", solid: true },
            24: { id: 24, name: "书架", color: "#8B4513", solid: true },
            25: { id: 25, name: "南瓜", color: "#FFA500", solid: true }
        };
        
        // 游戏状态
        let gameStarted = false;
        let socket = null;
        let currentUser = null;
        
        // 玩家对象 - 修改:初始位置在世界中央草坪上方10格
        let player = {
            username: "",
            x: 250 * BLOCK_SIZE,  // 世界中央 (世界宽度500/2 = 250)
            y: 50 * BLOCK_SIZE,   // 草坪在y=60,上方10格就是y=50
            color: "#3d85c6",
            selectedBlock: 1
        };
        
        // 其他玩家
        let otherPlayers = {};
        
        // 世界数据
        let worldData = {};
        
        // 按键状态
        let keys = { w: false, a: false, s: false, d: false };
        
        // 相机
        let camera = { x: 0, y: 0 };
        
        // 鼠标位置
        let mouse = { x: 0, y: 0 };
        
        // ==================== 初始化函数 ====================
        function init() {
            resizeCanvas();
            setupEventListeners();
            initBlockSelector();
            initSkinSelector();
            
            // 检查是否已经登录
            checkLoginStatus();
        }
        
        function resizeCanvas() {
            CANVAS_WIDTH = window.innerWidth;
            CANVAS_HEIGHT = window.innerHeight;
            canvas.width = CANVAS_WIDTH;
            canvas.height = CANVAS_HEIGHT;
        }
        
        function setupEventListeners() {
            window.addEventListener('resize', resizeCanvas);
            
            // 登录
            document.getElementById('login-btn').addEventListener('click', handleLogin);
            document.getElementById('password').addEventListener('keypress', (e) => {
                if (e.key === 'Enter') handleLogin();
            });
            
            // 游戏控制
            document.addEventListener('keydown', (e) => {
                const key = e.key.toLowerCase();
                if (key in keys) keys[key] = true;
                
                // T键打开聊天
                if (key === 't' || key === 'T') {
                    e.preventDefault();
                    document.getElementById('chat-input').focus();
                }
                
                // 数字键选择方块
                if (key >= '0' && key <= '9') {
                    const num = parseInt(key);
                    if (num <= 9) selectBlock(num);
                }
                
                // 扩展方块选择(使用shift+数字)
                if (e.shiftKey && key >= '0' && key <= '9') {
                    const num = parseInt(key) + 10;
                    if (BLOCK_TYPES[num]) selectBlock(num);
                }
            });
            
            document.addEventListener('keyup', (e) => {
                const key = e.key.toLowerCase();
                if (key in keys) keys[key] = false;
            });
            
            // 鼠标控制
            canvas.addEventListener('mousedown', handleMouseDown);
            canvas.addEventListener('contextmenu', (e) => e.preventDefault());
            canvas.addEventListener('mousemove', (e) => {
                const rect = canvas.getBoundingClientRect();
                mouse.x = e.clientX - rect.left;
                mouse.y = e.clientY - rect.top;
            });
            
            // 按钮控制
            document.getElementById('reset-world-btn').addEventListener('click', resetWorld);
            
            // 聊天输入
            document.getElementById('chat-input').addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    const message = e.target.value.trim();
                    if (message) {
                        sendChatMessage(message);
                        e.target.value = '';
                    }
                }
            });
        }
        
        function initBlockSelector() {
            const selector = document.getElementById('block-selector');
            selector.innerHTML = '';
            
            // 创建两行方块选择器
            for (let row = 0; row < 2; row++) {
                const rowDiv = document.createElement('div');
                rowDiv.style.display = 'flex';
                rowDiv.style.gap = '5px';
                rowDiv.style.marginBottom = '5px';
                
                for (let i = 1; i <= 9; i++) {
                    const blockId = row * 9 + i;
                    if (BLOCK_TYPES[blockId]) {
                        const option = document.createElement('div');
                        option.className = 'block-option';
                        option.style.backgroundColor = BLOCK_TYPES[blockId].color;
                        option.title = `${BLOCK_TYPES[blockId].name} (${blockId})`;
                        option.dataset.blockId = blockId;
                        
                        const number = document.createElement('span');
                        number.textContent = blockId;
                        number.style.color = 'white';
                        number.style.fontSize = '10px';
                        option.appendChild(number);
                        
                        option.addEventListener('click', () => selectBlock(blockId));
                        rowDiv.appendChild(option);
                    }
                }
                selector.appendChild(rowDiv);
            }
            
            selectBlock(1);
        }
        
        function initSkinSelector() {
            const skinOptions = document.querySelectorAll('.skin-option');
            skinOptions.forEach(option => {
                option.addEventListener('click', () => {
                    const color = option.dataset.color;
                    player.color = color;
                    
                    // 更新选中状态
                    skinOptions.forEach(opt => opt.classList.remove('selected'));
                    option.classList.add('selected');
                    
                    // 通知服务器皮肤变更
                    if (socket && socket.connected) {
                        socket.emit('player_update', {
                            x: player.x,
                            y: player.y,
                            color: player.color
                        });
                    }
                });
            });
            
            // 默认选中第一个皮肤
            skinOptions[0].classList.add('selected');
        }
        
        function checkLoginStatus() {
            fetch('/api/userinfo', {
                method: 'GET',
                credentials: 'include'
            })
            .then(response => response.json())
            .then(data => {
                if (data.logged_in) {
                    document.getElementById('login-overlay').style.display = 'none';
                    showLoading('加载世界数据...');
                    loadWorldData().then(() => {
                        showLoading(false);
                        initGame(data.username);
                    });
                }
            })
            .catch(error => {
                console.log('用户未登录,需要重新登录');
            });
        }
        
        // ==================== 登录系统 ====================
        function handleLogin() {
            const username = document.getElementById('username').value.trim();
            const password = document.getElementById('password').value.trim();
            const messageDiv = document.getElementById('login-message');
            
            if (!username || !password) {
                showMessage('用户名和密码不能为空', 'error');
                return;
            }
            
            showLoading('正在登录...');
            
            fetch('/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify({ username, password }),
                credentials: 'include'
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    showLoading('加载世界数据...');
                    return loadWorldData().then(() => {
                        document.getElementById('login-overlay').style.display = 'none';
                        showLoading(false);
                        initGame(username);
                    });
                } else {
                    throw new Error(data.error || '登录失败');
                }
            })
            .catch(error => {
                showLoading(false);
                showMessage(error.message, 'error');
            });
        }
        
        function getCSRFToken() {
            const meta = document.querySelector('meta[name="csrf-token"]');
            return meta ? meta.getAttribute('content') : '';
        }
        
        function showMessage(message, type = 'error') {
            const messageDiv = document.getElementById('login-message');
            messageDiv.textContent = message;
            messageDiv.style.color = type === 'error' ? '#ff6b6b' : '#4CAF50';
        }
        
        function showLoading(text = null) {
            const loadingScreen = document.getElementById('loading-screen');
            if (text) {
                loadingScreen.style.display = 'flex';
                document.getElementById('loading-text').textContent = text;
            } else {
                loadingScreen.style.display = 'none';
            }
        }
        
        // ==================== 游戏初始化 ====================
        function initGame(username) {
            player.username = username;
            document.getElementById('player-name').textContent = username;
            
            // 显示游戏界面
            document.getElementById('game-container').style.display = 'block';
            
            // 连接Socket.IO
            connectSocketIO();
            
            // 开始游戏循环
            gameStarted = true;
            gameLoop();
        }
        
        function connectSocketIO() {
            socket = io();
            
            socket.on('connect', () => {
                console.log('Socket.IO连接成功');
                
                // 发送玩家加入
                socket.emit('player_join', {
                    username: player.username,
                    color: player.color,
                    x: player.x,
                    y: player.y,
                    selectedBlock: player.selectedBlock
                });
            });
            
            socket.on('connected', (data) => {
                console.log('服务器确认连接', data);
            });
            
            socket.on('player_joined', (data) => {
                if (data.username !== player.username) {
                    addChatMessage('系统', `${data.username} 加入了游戏`, true);
                    otherPlayers[data.username] = {
                        username: data.username,
                        x: data.x,
                        y: data.y,
                        color: data.color || '#3d85c6'
                    };
                    updatePlayersList();
                }
            });
            
            socket.on('player_left', (data) => {
                if (data.username in otherPlayers) {
                    addChatMessage('系统', `${data.username} 离开了游戏`, true);
                    delete otherPlayers[data.username];
                    updatePlayersList();
                }
            });
            
            socket.on('player_updated', (data) => {
                if (data.username !== player.username && data.username in otherPlayers) {
                    otherPlayers[data.username].x = data.x;
                    otherPlayers[data.username].y = data.y;
                    if (data.color) otherPlayers[data.username].color = data.color;
                }
            });
            
            socket.on('block_updated', (data) => {
                const blockX = data.x;
                const blockY = data.y;
                const blockType = data.blockType;
                
                const chunkX = Math.floor(blockX / CHUNK_SIZE);
                const chunkY = Math.floor(blockY / CHUNK_SIZE);
                const localX = blockX % CHUNK_SIZE;
                const localY = blockY % CHUNK_SIZE;
                
                const chunkKey = `${chunkX},${chunkY}`;
                const blockKey = `${localX},${localY}`;
                
                if (!worldData[chunkKey]) worldData[chunkKey] = {};
                worldData[chunkKey][blockKey] = blockType;
            });
            
            socket.on('chat_message', (data) => {
                if (data.sender !== player.username) {
                    addChatMessage(data.sender, data.message);
                }
            });
            
            socket.on('players_list', (data) => {
                otherPlayers = {};
                data.players.forEach(p => {
                    if (p.username !== player.username) {
                        otherPlayers[p.username] = p;
                    }
                });
                updatePlayersList();
            });
        }
        
        function loadWorldData() {
            return fetch('/api/world_blocks', {
                method: 'GET',
                headers: {
                    'X-CSRFToken': getCSRFToken()
                },
                credentials: 'include'
            })
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }
                return response.json();
            })
            .then(data => {
                if (data.success) {
                    worldData = data.world_data;
                    console.log('世界数据加载完成,区块数:', Object.keys(worldData).length);
                } else {
                    throw new Error(data.error || '加载世界数据失败');
                }
            })
            .catch(error => {
                console.error('加载世界数据失败:', error);
                worldData = {};
            });
        }
        
        function resetWorld() {
            if (confirm('确定要重置世界吗?所有方块将被重置为默认地形。')) {
                fetch('/api/reset_world', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRFToken': getCSRFToken()
                    },
                    credentials: 'include'
                })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        alert('世界已重置,正在重新加载...');
                        loadWorldData().then(() => {
                            location.reload();
                        });
                    } else {
                        alert('重置失败: ' + data.error);
                    }
                })
                .catch(error => {
                    console.error('重置世界失败:', error);
                    alert('重置失败,请检查网络连接');
                });
            }
        }
        
        // ==================== 游戏逻辑 ====================
        function updatePlayer() {
            if (!gameStarted) return;
            
            let moveX = 0;
            let moveY = 0;
            
            if (keys['w']) moveY -= PLAYER_SPEED;
            if (keys['s']) moveY += PLAYER_SPEED;
            if (keys['a']) moveX -= PLAYER_SPEED;
            if (keys['d']) moveX += PLAYER_SPEED;
            
            if (moveX !== 0 && moveY !== 0) {
                moveX *= 0.7071;
                moveY *= 0.7071;
            }
            
            // 修改:取消重力,只用WASD控制
            // 检查碰撞
            if (moveX !== 0) {
                const newX = player.x + moveX;
                if (!checkCollision(newX, player.y, PLAYER_WIDTH, PLAYER_HEIGHT)) {
                    player.x = newX;
                }
            }
            
            if (moveY !== 0) {
                const newY = player.y + moveY;
                if (!checkCollision(player.x, newY, PLAYER_WIDTH, PLAYER_HEIGHT)) {
                    player.y = newY;
                }
            }
            
            // 更新UI
            document.getElementById('player-pos').textContent = 
                `${Math.floor(player.x)}, ${Math.floor(player.y)}`;
            
            // 更新相机
            camera.x = player.x - CANVAS_WIDTH / 2;
            camera.y = player.y - CANVAS_HEIGHT / 2;
            
            // 发送位置更新
            if (socket && socket.connected) {
                socket.emit('player_update', {
                    x: player.x,
                    y: player.y,
                    color: player.color
                });
            }
        }
        
        function checkCollision(x, y, width, height) {
            const left = Math.floor(x / BLOCK_SIZE);
            const right = Math.floor((x + width) / BLOCK_SIZE);
            const top = Math.floor(y / BLOCK_SIZE);
            const bottom = Math.floor((y + height) / BLOCK_SIZE);
            
            for (let bx = left; bx <= right; bx++) {
                for (let by = top; by <= bottom; by++) {
                    const blockType = getBlockAt(bx, by);
                    const block = BLOCK_TYPES[blockType];
                    
                    if (block && block.solid) {
                        const blockX = bx * BLOCK_SIZE;
                        const blockY = by * BLOCK_SIZE;
                        
                        if (x + width > blockX && 
                            x < blockX + BLOCK_SIZE && 
                            y + height > blockY && 
                            y < blockY + BLOCK_SIZE) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        
        function handleMouseDown(e) {
            if (!gameStarted || !socket) return;
            
            const rect = canvas.getBoundingClientRect();
            const mouseX = e.clientX - rect.left + camera.x;
            const mouseY = e.clientY - rect.top + camera.y;
            
            const blockX = Math.floor(mouseX / BLOCK_SIZE);
            const blockY = Math.floor(mouseY / BLOCK_SIZE);
            
            // 检查距离
            const distance = Math.sqrt(
                Math.pow(player.x - blockX * BLOCK_SIZE, 2) + 
                Math.pow(player.y - blockY * BLOCK_SIZE, 2)
            );
            
            if (distance > PLAYER_INTERACTION_RANGE) {
                return;
            }
            
            // 修改:简化点击逻辑
            // 左键:如果有方块就破坏,没有方块就放置
            // 右键:如果有方块就破坏,没有方块就放置
            const currentBlock = getBlockAt(blockX, blockY);
            
            if (currentBlock !== 0) {
                // 破坏方块(替换为空气)
                updateBlock(blockX, blockY, 0, true);
            } else {
                // 放置方块
                if (player.selectedBlock !== 0) {
                    updateBlock(blockX, blockY, player.selectedBlock, true);
                }
            }
        }
        
        function updateBlock(blockX, blockY, blockType, broadcast = false) {
            const chunkX = Math.floor(blockX / CHUNK_SIZE);
            const chunkY = Math.floor(blockY / CHUNK_SIZE);
            const localX = blockX % CHUNK_SIZE;
            const localY = blockY % CHUNK_SIZE;
            
            const chunkKey = `${chunkX},${chunkY}`;
            const blockKey = `${localX},${localY}`;
            
            if (!worldData[chunkKey]) worldData[chunkKey] = {};
            worldData[chunkKey][blockKey] = blockType;
            
            fetch('/api/update_block', {
                method: 'POST',
                headers: { 
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify({
                    x: blockX,
                    y: blockY,
                    blockType: blockType
                }),
                credentials: 'include'
            })
            .then(response => response.json())
            .then(data => {
                if (!data.success) {
                    console.error('保存方块失败:', data.error);
                }
            })
            .catch(error => {
                console.error('保存方块请求失败:', error);
            });
            
            if (broadcast && socket && socket.connected) {
                socket.emit('block_update', {
                    x: blockX,
                    y: blockY,
                    blockType: blockType
                });
            }
        }
        
        function getBlockAt(worldX, worldY) {
            const chunkX = Math.floor(worldX / CHUNK_SIZE);
            const chunkY = Math.floor(worldY / CHUNK_SIZE);
            const blockX = worldX % CHUNK_SIZE;
            const blockY = worldY % CHUNK_SIZE;
            
            const chunkKey = `${chunkX},${chunkY}`;
            const blockKey = `${blockX},${blockY}`;
            
            if (worldData[chunkKey] && worldData[chunkKey][blockKey] !== undefined) {
                return worldData[chunkKey][blockKey];
            }
            
            // 默认生成地形(如果没有数据)
            if (worldY >= 95) return 11; // 基岩
            if (worldY >= 70) return 3;  // 石头
            if (worldY >= 65) return 2;  // 泥土
            if (worldY === 60) return 1; // 草地
            return 0; // 空气
        }
        
        function selectBlock(blockId) {
            if (BLOCK_TYPES[blockId]) {
                player.selectedBlock = blockId;
                
                const options = document.querySelectorAll('.block-option');
                options.forEach(option => {
                    option.classList.remove('selected');
                    if (parseInt(option.dataset.blockId) === blockId) {
                        option.classList.add('selected');
                    }
                });
            }
        }
        
        // ==================== 渲染系统 ====================
        function render() {
            if (!gameStarted) return;
            
            ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
            
            drawSky();
            drawWorld();
            drawOtherPlayers();
            drawPlayer();
            
            // 绘制方块轮廓(如果鼠标在可交互范围内)
            drawBlockOutline();
        }
        
        function drawSky() {
            const gradient = ctx.createLinearGradient(0, 0, 0, CANVAS_HEIGHT);
            gradient.addColorStop(0, '#87CEEB');
            gradient.addColorStop(0.5, '#7ec0ee');
            gradient.addColorStop(1, '#5aa9e6');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        }
        
        function drawWorld() {
            const startChunkX = Math.floor(camera.x / (CHUNK_SIZE * BLOCK_SIZE)) - 1;
            const endChunkX = Math.floor((camera.x + CANVAS_WIDTH) / (CHUNK_SIZE * BLOCK_SIZE)) + 1;
            const startChunkY = Math.floor(camera.y / (CHUNK_SIZE * BLOCK_SIZE)) - 1;
            const endChunkY = Math.floor((camera.y + CANVAS_HEIGHT) / (CHUNK_SIZE * BLOCK_SIZE)) + 1;
            
            for (let cx = startChunkX; cx <= endChunkX; cx++) {
                for (let cy = startChunkY; cy <= endChunkY; cy++) {
                    drawChunk(cx, cy);
                }
            }
        }
        
        function drawChunk(chunkX, chunkY) {
            const screenX = chunkX * CHUNK_SIZE * BLOCK_SIZE - camera.x;
            const screenY = chunkY * CHUNK_SIZE * BLOCK_SIZE - camera.y;
            
            for (let bx = 0; bx < CHUNK_SIZE; bx++) {
                for (let by = 0; by < CHUNK_SIZE; by++) {
                    const worldX = chunkX * CHUNK_SIZE + bx;
                    const worldY = chunkY * CHUNK_SIZE + by;
                    const blockType = getBlockAt(worldX, worldY);
                    
                    if (blockType === 0) continue;
                    
                    const block = BLOCK_TYPES[blockType];
                    if (!block) continue;
                    
                    const x = screenX + bx * BLOCK_SIZE;
                    const y = screenY + by * BLOCK_SIZE;
                    
                    if (x + BLOCK_SIZE < 0 || x > CANVAS_WIDTH ||
                        y + BLOCK_SIZE < 0 || y > CANVAS_HEIGHT) {
                        continue;
                    }
                    
                    ctx.fillStyle = block.color;
                    ctx.fillRect(x, y, BLOCK_SIZE, BLOCK_SIZE);
                    
                    ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
                    ctx.lineWidth = 1;
                    ctx.strokeRect(x, y, BLOCK_SIZE, BLOCK_SIZE);
                }
            }
        }
        
        function drawPlayer() {
            const screenX = player.x - camera.x;
            const screenY = player.y - camera.y;
            
            ctx.fillStyle = player.color;
            ctx.fillRect(screenX, screenY, PLAYER_WIDTH, PLAYER_HEIGHT);
            
            ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
            ctx.lineWidth = 2;
            ctx.strokeRect(screenX, screenY, PLAYER_WIDTH, PLAYER_HEIGHT);
            
            ctx.fillStyle = 'white';
            ctx.fillRect(screenX + PLAYER_WIDTH - 8, screenY + 10, 4, 4);
            ctx.fillRect(screenX + 4, screenY + 10, 4, 4);
        }
        
        function drawOtherPlayers() {
            Object.values(otherPlayers).forEach(otherPlayer => {
                const screenX = otherPlayer.x - camera.x;
                const screenY = otherPlayer.y - camera.y;
                
                if (screenX + PLAYER_WIDTH < 0 || screenX > CANVAS_WIDTH ||
                    screenY + PLAYER_HEIGHT < 0 || screenY > CANVAS_HEIGHT) {
                    return;
                }
                
                ctx.fillStyle = otherPlayer.color || '#3d85c6';
                ctx.fillRect(screenX, screenY, PLAYER_WIDTH, PLAYER_HEIGHT);
                
                ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
                ctx.lineWidth = 2;
                ctx.strokeRect(screenX, screenY, PLAYER_WIDTH, PLAYER_HEIGHT);
                
                ctx.fillStyle = 'white';
                ctx.fillRect(screenX + PLAYER_WIDTH - 8, screenY + 10, 4, 4);
                ctx.fillRect(screenX + 4, screenY + 10, 4, 4);
                
                ctx.fillStyle = 'white';
                ctx.font = '12px Arial';
                ctx.textAlign = 'center';
                ctx.fillText(otherPlayer.username, screenX + PLAYER_WIDTH / 2, screenY - 5);
            });
        }
        
        function drawBlockOutline() {
            const mouseWorldX = mouse.x + camera.x;
            const mouseWorldY = mouse.y + camera.y;
            
            const blockX = Math.floor(mouseWorldX / BLOCK_SIZE);
            const blockY = Math.floor(mouseWorldY / BLOCK_SIZE);
            
            const screenX = blockX * BLOCK_SIZE - camera.x;
            const screenY = blockY * BLOCK_SIZE - camera.y;
            
            // 检查距离是否在交互范围内
            const distance = Math.sqrt(
                Math.pow(player.x - blockX * BLOCK_SIZE, 2) + 
                Math.pow(player.y - blockY * BLOCK_SIZE, 2)
            );
            
            if (distance <= PLAYER_INTERACTION_RANGE) {
                ctx.strokeStyle = '#FFFF00';
                ctx.lineWidth = 2;
                ctx.setLineDash([5, 3]);
                ctx.strokeRect(screenX, screenY, BLOCK_SIZE, BLOCK_SIZE);
                ctx.setLineDash([]);
            }
        }
        
        // ==================== UI系统 ====================
        function addChatMessage(sender, message, isSystem = false) {
            const chatMessages = document.getElementById('chat-messages');
            const messageDiv = document.createElement('div');
            messageDiv.className = 'chat-message';
            
            if (isSystem) {
                messageDiv.textContent = `[系统] ${message}`;
                messageDiv.style.color = '#8f94fb';
                messageDiv.style.fontStyle = 'italic';
            } else {
                messageDiv.textContent = `${sender}: ${message}`;
            }
            
            chatMessages.appendChild(messageDiv);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }
        
        function sendChatMessage(message) {
            addChatMessage(player.username, message);
            
            if (socket && socket.connected) {
                socket.emit('chat_message', {
                    sender: player.username,
                    message: message
                });
            }
        }
        
        function updatePlayersList() {
            const playersList = document.getElementById('players-list');
            const onlineCount = Object.keys(otherPlayers).length + 1;
            
            document.getElementById('online-count').textContent = onlineCount;
            
            playersList.innerHTML = `
                <div style="font-weight: bold; margin-bottom: 10px;">
                    在线玩家 (${onlineCount})
                </div>
                <div class="player-item">
                    <span class="player-color" style="background: ${player.color}"></span>
                    <span>${player.username} (你)</span>
                </div>
            `;
            
            Object.values(otherPlayers).forEach(otherPlayer => {
                const playerItem = document.createElement('div');
                playerItem.className = 'player-item';
                playerItem.innerHTML = `
                    <span class="player-color" style="background: ${otherPlayer.color || '#3d85c6'}"></span>
                    <span>${otherPlayer.username}</span>
                `;
                playersList.appendChild(playerItem);
            });
        }
        
        // ==================== 游戏主循环 ====================
        function gameLoop() {
            if (!gameStarted) return;
            
            updatePlayer();
            render();
            
            requestAnimationFrame(gameLoop);
        }
        
        // ==================== 启动游戏 ====================
        window.addEventListener('load', init);
    </script>
</body>
</html>

特别注意,此项目是基于服务器运行的,单机无法运行,所以,还是来到ourcraft.xin,注册一个账号,来到"自由建造"栏目吧

可以看到是可以联机的

只有知名度提升上去了,才有更多的玩家来玩,才可以联机

相关推荐
多米Domi0115 小时前
0x3f第33天复习 (16;45-18:00)
数据结构·python·算法·leetcode·链表
freepopo6 小时前
天津商业空间设计:材质肌理里的温度与质感[特殊字符]
python·材质
森叶6 小时前
Java 比 Python 高性能的原因:重点在高并发方面
java·开发语言·python
小二·6 小时前
Python Web 开发进阶实战:混沌工程初探 —— 主动注入故障,构建高韧性系统
开发语言·前端·python
Lkygo7 小时前
LlamaIndex使用指南
linux·开发语言·python·llama
小二·7 小时前
Python Web 开发进阶实战:低代码平台集成 —— 可视化表单构建器 + 工作流引擎实战
前端·python·低代码
Wise玩转AI7 小时前
团队管理:AI编码工具盛行下,如何防范设计能力退化与知识浅薄化?
python·ai编程·ai智能体·开发范式
赵谨言7 小时前
Python串口的三相交流电机控制系统研究
大数据·开发语言·经验分享·python
座山雕~7 小时前
Springboot
android·spring boot·后端
鹿角片ljp8 小时前
Engram 论文精读:用条件记忆模块重塑稀疏大模型
python·自然语言处理·nlp