PathfinderTales 是一款基于AI生成的互动文本冒险游戏,玩家可以选择不同的故事类型(科幻、末世、奇幻),通过做出选择来影响故事发展,体验个性化的叙事旅程。



## 功能特性 ✨
- 🎮 多故事类型:支持科幻、末世、奇幻三种故事类型
- 🤖 AI生成内容:使用DeepSeek API动态生成故事情节和选项
- 💾 进度保存:自动保存游戏进度和用户选择
- 📚 历史记录:查看过往游戏记录并继续游玩
- 🎨 响应式设计:适配桌面和移动设备的现代化UI
- 🔄 实时交互:流畅的故事生成和选择体验


## 📂 项目结构、
- 📁 PathfinderTales/
  - 📄 app.py # Flask应用主入口
  - 📄 config.py # 应用配置文件
  - 📄 extensions.py # Flask扩展初始化
  - 📄 README.md # 项目说明
  - 📁 database/ # 数据库模块
      - 📄 models.py #数据模型
      - 📄 db_operations.py # 数据库CRUD操作
  - 📁 routes/ # 路由模块
      - 📄 __init__.py 
      - 📄 api.py # API路由
      - 📄 game.py # 游戏相关路由
      - 📄 main.py # 主路由
  - 📁 static/ # 静态资源
      - 📁 css/ # 样式文件
      - 📁 js/ # JavaScript文件
  - 📁 templates/ # Jinja2模板
      - 📄 index.html # 首页
      - 📄 history.html # 历史记录页面
      - 📄 game.html # 游戏主界面
      - 📄 structure.html # 数据结构页面
  - 📄 .env # 项目环境变量文件 
  - 📄 app.db # 系统生成的SQLite数据库文件
  - 📄 models.py # 数据库模型
  - 📄 openai_api.py # OpenAI API调用封装
  - 📄 requirements.txt # 项目依赖包列表


## 🚀 快速开始

# 📦 依赖安装指南

## 系统要求
- Python 3.13+
- pip 20.0+
- 推荐使用虚拟环境

## 基础安装

### 1. 创建虚拟环境(推荐)
```bash
python -m venv env
source env/bin/activate  # Linux/Mac
env\Scripts\activate    # Windows
```

### 2.安装依赖命令:
```bash
pip install -r requirements.txt

```

### 3. 运行项目
```bash
python app.py
```

## 🔧 技术栈

### 后端
- **Flask**: Python轻量级Web框架
- **SQLAlchemy**: Python SQL工具包和ORM
- **OpenAI API**: 用于生成故事内容

### 前端
- **HTML5**: 页面结构
- **CSS3**: 样式设计,包含响应式布局
- **JavaScript**: 交互逻辑
- **AJAX**: 异步数据交互

### 数据库
- **SQLite**: 轻量级数据库,用于存储游戏数据和用户选择

---

## 📊 数据模型

### `UserGame` (用户游戏)
| 字段名         | 说明               |
|----------------|--------------------|
| `id`           | 主键               |
| `session_id`   | 游戏会话ID         |
| `story_type`   | 故事类型           |
| `current_act`  | 当前幕             |
| `current_scene`| 当前关卡           |
| `is_completed` | 是否完成           |

### `StoryContent` (故事内容)
| 字段名       | 说明               |
|--------------|--------------------|
| `id`         | 主键               |
| `game_id`    | 关联的游戏ID       |
| `act`        | 幕数               |
| `scene`      | 关卡数             |
| `content`    | 故事内容           |
| `choices`    | 选项列表 (JSON格式)|

### `UserChoice` (用户选择)
| 字段名         | 说明               |
|----------------|--------------------|
| `id`           | 主键               |
| `game_id`      | 关联的游戏ID       |
| `act`          | 幕数               |
| `scene`        | 关卡数             |
| `choice_text`  | 选择的文本         |

---

## 🔌 API接口

### `POST /api/generate_content`
生成新的故事内容

**请求体示例:**
```json
{
  "act": 1,
  "scene": 1
}
```

### `POST /api/make_choice`
保存用户选择并推进故事

**请求体示例:**
```json
{
  "choice_text": "探索周围环境",
  "act": 1,
  "scene": 1
}
```

### `POST /api/get_story_progress`
获取当前游戏进度和内容

一、数据库设计与初始化

整个项目出于简化与快速实现的目的,仅需要三张表即可完成核心的游戏功能需求:一张用来存储游戏会话信息(包含会话id、故事题材、当前幕次、场景及等字段,用来记录不同的游戏会话);一张用来存放剧情内容信息(包含id,关联游戏id、幕次、场景、剧情文本、选项JSON等字段,用来记录每个剧情故事中的节点信息);一张用来存放用户选择记录(包含id,关联游戏id、幕次、场景、选项文本等字段,用来记录用户在不同剧情故事,不同节点中做出的选择)。

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()


class UserGame(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    session_id = db.Column(db.String(100), unique=True, nullable=False)
    story_type = db.Column(db.String(50), nullable=False)
    current_act = db.Column(db.Integer, default=1)
    current_scene = db.Column(db.Integer, default=1)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    is_completed = db.Column(db.Boolean, default=False)

    # 关系
    story_contents = db.relationship('StoryContent', backref='game', lazy=True)
    choices = db.relationship('UserChoice', backref='game', lazy=True)


class StoryContent(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    game_id = db.Column(db.Integer, db.ForeignKey('user_game.id'), nullable=False)
    act = db.Column(db.Integer, nullable=False)
    scene = db.Column(db.Integer, nullable=False)
    content = db.Column(db.Text, nullable=False)
    choices = db.Column(db.Text, nullable=False)  # 存储选项列表的 JSON 字符串
    generated_at = db.Column(db.DateTime, default=datetime.utcnow)

    # 可选:添加唯一性约束,防止重复生成同一幕关卡
    __table_args__ = (db.UniqueConstraint('game_id', 'act', 'scene', name='_game_act_scene_uc'),)


class UserChoice(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    game_id = db.Column(db.Integer, db.ForeignKey('user_game.id'), nullable=False)
    act = db.Column(db.Integer, nullable=False)
    scene = db.Column(db.Integer, nullable=False)
    choice_text = db.Column(db.String(200), nullable=False)
    selected_at = db.Column(db.DateTime, default=datetime.utcnow)

二、AI模型API调用接口的实现

目前主流AI大模型大多支持遵循OpenAI调用规范,可借助openai库封装一套通用API调用模块。这样不仅能够使用统一的接口调用AI服务,也有利于项目后续灵活接入扩展更多AI大模型。

# 初始化OpenAI配置
from openai import OpenAI
from config import Config



def call_deepseek_api(story_type, act, scene, previous_choices=None):
    """
    使用OpenAI库调用DeepSeek API生成故事内容

    参数:
    - story_type: 故事类型
    - act: 当前幕
    - scene: 当前关卡
    - previous_choices: 之前的选择列表
    """
    client = OpenAI(api_key=Config.DEEPSEEK_API_KEY, base_url=Config.DEEPSEEK_API_URL)
    # 构建系统提示
    system_prompt = f"""你是一个专业的{story_type}故事作家。请根据用户的要求生成一个引人入胜的故事片段和3个不同的选择选项。
    故事应该符合{story_type}类型的风格,并且与之前的情节保持连贯性。

    生成格式要求:
    1. 首先是一个故事段落(300-800字左右)
    2. 然后是一行"OPTIONS:"作为分隔符
    3. 最后是3个不同的选择选项,每个选项用"- "开头,单独一行

    示例:
    '你站在一个废弃的空间站控制室,屏幕上闪烁着警告信号。外面的星空异常安静,但你知道危险可能随时降临。你需要决定下一步行动。
    OPTIONS:
    - 检查控制台获取更多信息
    - 前往气闸舱准备离开
    - 搜索站内是否有其他幸存者'
    """

    # 构建用户提示
    if act == 1 and scene == 1:
        user_prompt = f"生成{story_type}故事的开幕内容,创建一个引人入胜的开场。"
    elif act == 5 and scene == 3:
        user_prompt = f"生成{story_type}故事的终幕结局内容,基于之前的选择创造一个令人满意的结局。不要提供任何选项,故事应该自然结束。"
    else:
        user_prompt = f"生成{story_type}故事的第{act}幕第{scene}关内容。"

    # 添加上下文
    if previous_choices:
        user_prompt += f" 之前的选择包括: {', '.join(previous_choices)}"

    try:
        response =   client.chat.completions.create(
            model="deepseek-chat",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.8
        )

        # 解析响应
        full_content = response.choices[0].message.content

        # 如果是终幕,不需要选项
        if act == 5 and scene == 3:
            return full_content, []

        # 分离故事内容和选项
        if "OPTIONS:" in full_content:
            parts = full_content.split("OPTIONS:")
            story_content = parts[0].strip()
            options_text = parts[1].strip()

            # 提取选项
            options = []
            for line in options_text.split("\n"):
                line = line.strip()
                if line.startswith("- "):
                    options.append(line[2:].strip())

            # 确保有3个选项
            while len(options) < 3:
                options.append(f"选项 {len(options) + 1}")

            return story_content, options[:3]
        else:
            # 如果没有找到OPTIONS分隔符,使用默认选项
            default_options = {
                1: ["探索周围环境", "检查设备状态", "发送求救信号"],
                2: ["悄悄观察", "主动发出信号", "准备防御"],
                3: ["寻找其他入口", "尝试破解封锁", "制造分散注意力的声音"]
            }
            return full_content, default_options.get(scene, default_options[1])

    except Exception as e:
        print(f"API调用错误: {e}")

        # API调用失败时使用备用内容
        backup_stories = {
            "sci-fi": {
                "opening": "在遥远的未来,人类已经掌握了星际旅行技术。你作为探索者,被派往一个未知的星球执行任务。",
                "act1": [
                    "你的飞船突然遭遇了一场离子风暴,被迫降落在陌生的星球上。",
                    "你发现这个星球上有着高度发达但已废弃的古代文明遗迹。",
                    "你找到了一个仍然运作的控制台,上面显示着神秘的符号和地图。"
                ],
                "act2": [
                    "你决定探索遗迹深处,发现了一个隐藏的地下设施。",
                    "设施中保存着关于这个星球文明的惊人记录,但也触发了安全系统。",
                    "你成功破解了安全系统,获得了访问核心数据库的权限。"
                ],
                "act3": [
                    "核心数据库揭示了星球文明的最终命运和一个即将到来的宇宙危机。",
                    "你必须决定如何利用这些信息,是警告人类还是尝试修复星球的防御系统。",
                    "基于你的选择,人类文明要么获得了新的希望,要么面临着更大的危机。"
                ],
                "ending": "你的决定改变了人类的命运。无论结果如何,你的勇气和智慧将被永远铭记。"
            },
            "apocalypse": {
                "opening": "世界末日来临,丧尸横行。你躲在一个废弃的超市里,思考着下一步该怎么办。",
                "act1": [
                    "你听到外面有动静,似乎有其他幸存者正在接近。",
                    "你发现了一个地下避难所,但入口被封锁了。",
                    "你成功进入了避难所,发现里面有一些有用的物资和线索。"
                ],
                "act2": [
                    "根据线索,你决定前往城市的医院寻找疫苗研究资料。",
                    "医院里充满了危险,但你找到了关键的研究数据。",
                    "你遇到了另一组幸存者,他们也有自己的计划和目标。"
                ],
                "act3": [
                    "你必须决定是否与这些幸存者合作,还是独自行动。",
                    "你的决定影响了疫苗研究的进展和幸存者社区的命运。",
                    "最终,你找到了一个可能重建文明的方法,但这需要巨大的牺牲。"
                ],
                "ending": "末日之后的世界依然残酷,但你的行动为人类带来了一线希望。新的文明或许将从你的选择中诞生。"
            },
            "fantasy": {
                "opening": "你是一个年轻的冒险者,在一个古老的魔法世界中寻找失落的宝藏。",
                "act1": [
                    "你发现了一张神秘的地图,指引你前往一个被遗忘的神庙。",
                    "在神庙入口,你遇到了一个古老的守护者,它考验你的智慧和勇气。",
                    "你成功通过了考验,获得了进入神庙深处的资格。"
                ],
                "act2": [
                    "神庙深处隐藏着强大的魔法物品和古老的秘密。",
                    "你发现这些物品与一个即将苏醒的古老邪恶有关。",
                    "你必须决定如何使用这些物品,是封印邪恶还是利用它的力量。"
                ],
                "act3": [
                    "你的决定引发了连锁反应,改变了魔法世界的平衡。",
                    "你面临着最终的考验,必须做出牺牲来保护你所珍视的一切。",
                    "基于你的选择,魔法世界要么迎来了新的黄金时代,要么陷入了更深的黑暗。"
                ],
                "ending": "你的传奇将被吟游诗人传唱数个世纪。无论是作为英雄还是反派,你的名字将永远铭刻在历史中。"
            }
        }

        # 根据幕和关卡选择内容
        if act == 1 and scene == 1:
            content = backup_stories.get(story_type, backup_stories["sci-fi"])["opening"]
            options = ["探索周围环境", "检查设备状态", "发送求救信号"]
        elif act == 5 and scene == 3:
            content = backup_stories.get(story_type, backup_stories["sci-fi"])["ending"]
            options = []
        else:
            act_key = f"act{act}"
            scene_index = min(scene - 1, 2)  # 确保索引在0-2范围内
            content = backup_stories.get(story_type, backup_stories["sci-fi"])[act_key][scene_index]
            options = [
                ["探索周围环境", "检查设备状态", "发送求救信号"],
                ["悄悄观察", "主动发出信号", "准备防御"],
                ["寻找其他入口", "尝试破解封锁", "制造分散注意力的声音"]
            ][scene_index]

        return content, options

三、故事剧情交互路由蓝图的实现

为了提高代码的可维护性和模块化程度,将故事剧情相关的数据交互及处理操作都统一存放到api.py蓝图路由中进行管理。

from flask import Blueprint, request, jsonify, session
from models import UserGame, StoryContent, UserChoice, db
from openai_api import call_deepseek_api
import json

api_bp = Blueprint('api', __name__, url_prefix='/api')

@api_bp.route('/generate_content', methods=['POST'])
def generate_content():
    game_session_id = session.get('game_session_id')
    if not game_session_id:
        return jsonify({'error': 'No active game session'}), 400

    game = UserGame.query.filter_by(session_id=game_session_id).first()
    if not game:
        return jsonify({'error': 'Game not found'}), 404

    data = request.get_json()
    act = data.get('act', game.current_act)
    scene = data.get('scene', game.current_scene)

    # 检查是否已经存在该幕和关卡的内容
    existing_content = StoryContent.query.filter_by(
        game_id=game.id,
        act=act,
        scene=scene
    ).first()

    # 如果内容已存在,直接返回
    if existing_content:
        return jsonify({
            'content': existing_content.content,
            'choices': json.loads(existing_content.choices),
            'act': act,
            'scene': scene
        })

    # 获取之前的选择作为上下文
    previous_choices = []
    if act > 1 or scene > 1:
        # 获取之前所有关卡的选择
        previous_choices_records = UserChoice.query.filter_by(
            game_id=game.id
        ).filter(
            (UserChoice.act < act) |
            ((UserChoice.act == act) & (UserChoice.scene < scene))
        ).all()

        previous_choices = [choice.choice_text for choice in previous_choices_records]

    # 调用API生成内容
    content, choices = call_deepseek_api(game.story_type, act, scene, previous_choices)

    # 保存生成的内容
    story_content = StoryContent(
        game_id=game.id,
        act=act,
        scene=scene,
        content=content,
        choices=json.dumps(choices, ensure_ascii=False)
    )
    db.session.add(story_content)
    db.session.commit()

    return jsonify({
        'content': content,
        'choices': choices,
        'act': act,
        'scene': scene
    })


@api_bp.route('/make_choice', methods=['POST'])
def make_choice():
    game_session_id = session.get('game_session_id')
    if not game_session_id:
        return jsonify({'error': 'No active game session'}), 400

    game = UserGame.query.filter_by(session_id=game_session_id).first()
    if not game:
        return jsonify({'error': 'Game not found'}), 404

    data = request.get_json()
    choice_text = data.get('choice_text')
    act = data.get('act', game.current_act)
    scene = data.get('scene', game.current_scene)

    # 保存用户选择
    user_choice = UserChoice(
        game_id=game.id,
        act=act,
        scene=scene,
        choice_text=choice_text
    )
    db.session.add(user_choice)

    # 更新游戏进度
    if scene < 3:  # 每幕最多3个关卡
        game.current_scene = scene + 1
    else:
        if act < 5:  # 总共5幕
            game.current_act = act + 1
            game.current_scene = 1
        else:
            game.is_completed = True

    db.session.commit()

    return jsonify({
        'success': True,
        'next_act': game.current_act,
        'next_scene': game.current_scene,
        'is_completed': game.is_completed
    })


@api_bp.route('/get_story_progress', methods=['GET'])
def get_story_progress():
    game_session_id = session.get('game_session_id')
    if not game_session_id:
        return jsonify({'error': 'No active game session'}), 400

    game = UserGame.query.filter_by(session_id=game_session_id).first()
    if not game:
        return jsonify({'error': 'Game not found'}), 404

    # 获取所有故事内容
    story_contents = StoryContent.query.filter_by(game_id=game.id).all()
    contents = []
    for sc in story_contents:
        contents.append({
            'act': sc.act,
            'scene': sc.scene,
            'content': sc.content,
            'choices': json.loads(sc.choices),
            'generated_at': sc.generated_at.isoformat()
        })

    # 获取所有用户选择
    user_choices = UserChoice.query.filter_by(game_id=game.id).all()
    choices = []
    for uc in user_choices:
        choices.append({
            'act': uc.act,
            'scene': uc.scene,
            'choice_text': uc.choice_text,
            'selected_at': uc.selected_at.isoformat()
        })

    return jsonify({
        'story_type': game.story_type,
        'current_act': game.current_act,
        'current_scene': game.current_scene,
        'is_completed': game.is_completed,
        'contents': contents,
        'choices': choices
    })

四、游戏界面交互逻辑的实现

游戏页面采用左右分栏布局,其中左侧区域为幕次与关卡列表,用户可通过此列表切换查看已体验过的剧情内容,实现剧情回顾功能;右侧区域则为剧情展示区域,该区域进一步划分为上下两部分,上方显示当前关卡的剧情文本,下方则会根据游戏状态动态呈现用户操作选项(包括推动剧情发展的剧情分支选项、回顾剧情时的返回当前场景操作选项,亦或是故事完结后的重新开始选项)。

主要用来发送请求到后端接口获取AI生成的剧情故事,实行数据的渲染显示,以及剧情回顾功能的实现。

document.addEventListener('DOMContentLoaded', function () {
            const storyContent = document.getElementById('story-content');
            const choicesContainer = document.getElementById('choices-container');
            const startGameBtn = document.getElementById('start-game-btn');
            const loadingModal = document.getElementById('loading-modal');
            const actSceneList = document.getElementById('act-scene-list');
            const storyTypeBadge = document.getElementById('story-type-badge');

            let currentAct = 1;
            let currentScene = 1;
            let isGameCompleted = false;
            let storyProgressData = null;
            let currentView = 'current'; // 当前视图模式:current-当前场景,past-过往场景

            // 初始化幕和关卡列表
            initActSceneList();
            updateActSceneList();

            // 初始化幕和关卡列表
            function initActSceneList() {
                // 为每个幕添加点击事件
                document.querySelectorAll('.act-header').forEach(header => {
                    header.addEventListener('click', function () {
                        const actItem = this.parentElement;
                        actItem.classList.toggle('expanded');
                    });
                });

                // 为每个场景项添加点击事件
                actSceneList.addEventListener('click', function (e) {
                    const sceneItem = e.target.closest('.scene-item');
                    if (sceneItem) {
                        const act = parseInt(sceneItem.getAttribute('data-act'));
                        const scene = parseInt(sceneItem.getAttribute('data-scene'));

                        // 检查是否可以查看该场景(已完成或当前场景)
                        if (canViewScene(act, scene)) {
                            viewPastScene(act, scene);
                        }
                    }
                });

                // 默认展开当前幕
                document.querySelector('.act-item').classList.add('expanded');
            }

            // 检查是否可以查看指定场景
            function canViewScene(act, scene) {
                if (!storyProgressData || !storyProgressData.contents) return false;

                // 当前场景或已完成的场景可以查看
                const isCurrent = (act === currentAct && scene === currentScene);
                const isCompleted = storyProgressData.contents.some(c => c.act === act && c.scene === scene);

                return isCurrent || isCompleted;
            }

            // 查看过往场景
            function viewPastScene(act, scene) {
                if (!storyProgressData || !storyProgressData.contents) return;

                currentView = 'past';

                // 查找指定场景的内容
                const sceneContent = storyProgressData.contents.find(c => c.act === act && c.scene === scene);
                if (!sceneContent) return;

                // 查找该场景的选择
                const sceneChoices = storyProgressData.choices.filter(c => c.act === act && c.scene === scene);
                const selectedChoice = sceneChoices.length > 0 ? sceneChoices[0].choice_text : null;

                // 显示场景内容
                let sceneTitle = "";
                if (act === 1 && scene === 1) {
                    sceneTitle = "开幕";
                } else if (act === 5 && scene === 3) {
                    sceneTitle = "终幕结局";
                } else {
                    sceneTitle = `第${act}幕 - 第${scene}关`;
                }

                storyContent.innerHTML = `
                    <div class="story-scene">
                        <div class="scene-header">
                            <h1 class="scene-title">${sceneTitle}(回顾)</h1>
                            <p class="scene-subtitle">${getStoryType()}故事</p>
                        </div>
                        <div class="story-text">
                            <p>${sceneContent.content.replace(/\n/g, '</p><p>')}</p>
                            ${selectedChoice ? `<p class="selected-choice">你的选择: <strong>${selectedChoice}</strong></p>` : ''}
                        </div>
                    </div>
                `;

                // 显示返回当前场景的按钮
                if (!(act === currentAct && scene === currentScene)) {
                    choicesContainer.innerHTML = `
                        <div class="choices-title">
                            <p>这是过往的场景记录</p>
                        </div>
                        <div class="choices-grid">
                            <div class="choice-card" id="return-current-btn">
                                <div class="choice-icon">↩️</div>
                                <div class="choice-text">返回当前场景</div>
                                <div class="choice-number">点击返回</div>
                            </div>
                        </div>
                    `;

                    document.getElementById('return-current-btn').addEventListener('click', () => {
                        currentView = 'current';
                        showCurrentScene();
                    });
                } else {
                    // 如果是当前场景,正常显示选择
                    currentView = 'current';
                    showCurrentScene();
                }
            }

            function showCurrentScene() {
                if (!storyProgressData || !storyProgressData.contents) return;

                // 1. 查找当前场景的内容
                const currentContent = storyProgressData.contents.find(c => c.act === currentAct && c.scene === currentScene);
                if (!currentContent) return;

                const currentContentText = currentContent.content.replace(/\n/g, '</p><p>');

                // 2. 获取当前场景的 choices
                const availableChoices = currentContent.choices || [];

                displayStory(currentContentText, availableChoices);
            }

            // 开始游戏
            startGameBtn.addEventListener('click', function () {
                startGame();
            });

            // 开始游戏
            function startGame() {
                showLoading();
                updateProgress(0);

                // 模拟进度更新
                let progress = 0;
                const progressInterval = setInterval(() => {
                    progress += 5;
                    updateProgress(progress);

                    if (progress >= 100) {
                        clearInterval(progressInterval);
                        generateContent(1, 1);
                    }
                }, 200);
            }

            // 生成故事内容
            function generateContent(act, scene) {
                fetch('/api/generate_content', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        act: act,
                        scene: scene
                    })
                })
                    .then(response => response.json())
                    .then(data => {
                        hideLoading();
                        // 显示剧情内容
                        displayStory(data.content, data.choices);
                        // 更新幕和关卡列表
                        updateActSceneList();
                    })
                    .catch(error => {
                        hideLoading();
                        console.error('Error:', error);
                        alert('生成故事时出错,请重试');
                    });
            }

            // 显示故事内容
            function displayStory(content, choices) {
                let sceneTitle = "";
                if (currentAct === 1 && currentScene === 1) {
                    sceneTitle = "开幕";
                } else if (currentAct === 5 && currentScene === 3) {
                    sceneTitle = "终幕结局";
                } else {
                    sceneTitle = `第${currentAct}幕 - 第${currentScene}关`;
                }

                storyContent.innerHTML = `
                    <div class="story-scene">
                        <div class="scene-header">
                            <h1 class="scene-title">${sceneTitle}</h1>
                            <p class="scene-subtitle">${getStoryType()}故事</p>
                        </div>
                        <div class="story-text">
                            <p>${content.replace(/\n/g, '</p><p>')}</p>
                        </div>
                    </div>
                `;

                renderChoiceCards(choices);
            }

            // 渲染选择卡片
            function renderChoiceCards(choices) {
                choicesContainer.innerHTML = '';

                if (choices && choices.length > 0) {
                    // 添加选择标题
                    const choicesTitle = document.createElement('div');
                    choicesTitle.className = 'choices-title';
                    choicesTitle.textContent = '请做出你的选择:';
                    choicesContainer.appendChild(choicesTitle);

                    // 创建选择卡片容器
                    const choicesGrid = document.createElement('div');
                    choicesGrid.className = 'choices-grid';
                    choicesContainer.appendChild(choicesGrid);

                    // 添加选择卡片
                    choices.forEach((choice, index) => {
                        const card = document.createElement('div');
                        card.className = 'choice-card';
                        card.innerHTML = `
                            <div class="choice-icon">${getChoiceIcon(index)}</div>
                            <div class="choice-text">${choice}</div>
                            <div class="choice-number">选项 ${index + 1}</div>
                        `;
                        card.addEventListener('click', () => {
                            makeChoice(choice);
                        });
                        choicesGrid.appendChild(card);
                    });
                } else {
                    // 游戏结束
                    const endMessage = document.createElement('div');
                    endMessage.innerHTML = `
                        <div class="choices-title">
                            <h3>故事结束</h3>
                            <p>感谢您游玩文本冒险游戏!</p>
                        </div>
                        <div class="choices-grid">
                            <div class="choice-card" id="restart-btn">
                                <div class="choice-icon">🔄</div>
                                <div class="choice-text">重新开始</div>
                                <div class="choice-number">点击重启</div>
                            </div>
                        </div>
                    `;
                    choicesContainer.appendChild(endMessage);

                    document.getElementById('restart-btn').addEventListener('click', () => {
                        window.location.href = '/';
                    });

                    isGameCompleted = true;
                }
            }

            // 获取选择图标
            function getChoiceIcon(index) {
                const icons = ['🔍', '⚡', '🗺️', '🛡️', '💡', '🚀'];
                return icons[index] || '❓';
            }

            // 处理用户选择
            function makeChoice(choiceText) {
                showLoading();
                updateProgress(0);

                fetch('/api/make_choice', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        choice_text: choiceText,
                        act: currentAct,
                        scene: currentScene
                    })
                })
                    .then(response => response.json())
                    .then(data => {
                        if (data.is_completed) {
                            // 游戏结束
                            hideLoading();
                            displayStory("恭喜您完成了整个故事!", []);
                        } else {
                            // 显示幕间过渡动画
                            if (data.next_act > currentAct) {
                                showActTransition(data.next_act);
                            } else {
                                // 模拟进度更新
                                let progress = 0;
                                const progressInterval = setInterval(() => {
                                    progress += 10;
                                    updateProgress(progress);

                                    if (progress >= 100) {
                                        clearInterval(progressInterval);
                                        generateContent(data.next_act, data.next_scene);
                                    }
                                }, 200);
                            }
                        }
                    })
                    .catch(error => {
                        hideLoading();
                        console.error('Error:', error);
                        alert('做出选择时出错,请重试');
                    });
            }

            // 显示幕间过渡
            function showActTransition(nextAct) {
                const transition = document.createElement('div');
                transition.className = 'scene-transition active';
                transition.innerHTML = `<h2>第${nextAct}幕</h2>`;
                document.body.appendChild(transition);

                setTimeout(() => {
                    transition.classList.remove('active');
                    setTimeout(() => {
                        document.body.removeChild(transition);

                        // 模拟进度更新
                        let progress = 0;
                        const progressInterval = setInterval(() => {
                            progress += 10;
                            updateProgress(progress);

                            if (progress >= 100) {
                                clearInterval(progressInterval);
                                generateContent(nextAct, 1);
                            }
                        }, 200);
                    }, 500);
                }, 2000);
            }

            // 更新幕和关卡列表
            function updateActSceneList() {
                // 从服务器获取最新进度
                fetch('/api/get_story_progress')
                    .then(response => response.json())
                    .then(data => {
                        // 保存进度数据
                        storyProgressData = data;
                        currentAct = data.current_act;
                        currentScene = data.current_scene;

                        // 更新故事类型徽章
                        if (data.story_type) {
                            storyTypeBadge.textContent = getStoryTypeName(data.story_type);
                        }

                        // 重置所有场景状态
                        document.querySelectorAll('.scene-item').forEach(item => {
                            item.classList.remove('completed', 'current');
                            // 移除之前的选择文本
                            const choiceSpan = item.querySelector('.choice-history');
                            if (choiceSpan) {
                                choiceSpan.remove();
                            }
                        });

                        // 设置每个场景项的data-act和data-scene属性
                        const actItems = document.querySelectorAll('.act-item');
                        actItems.forEach((actItem, actIndex) => {
                            const sceneItems = actItem.querySelectorAll('.scene-item');
                            sceneItems.forEach((sceneItem, sceneIndex) => {
                                sceneItem.setAttribute('data-act', actIndex + 1);
                                sceneItem.setAttribute('data-scene', sceneIndex + 1);
                            });
                        });

                        // 标记已完成和当前场景
                        if (data.contents && data.contents.length > 0) {
                            data.contents.forEach(content => {
                                const sceneItem = findSceneItem(content.act, content.scene);
                                if (sceneItem) {
                                    sceneItem.classList.add('completed');
                                }
                            });
                        }

                        // 标记当前场景
                        const currentSceneItem = findSceneItem(data.current_act, data.current_scene);
                        if (currentSceneItem) {
                            currentSceneItem.classList.add('current');

                            // 确保当前幕是展开的
                            const actItem = currentSceneItem.closest('.act-item');
                            if (actItem && !actItem.classList.contains('expanded')) {
                                actItem.classList.add('expanded');
                            }
                        }

                        // 更新选择历史
                        if (data.choices && data.choices.length > 0) {
                            data.choices.forEach(choice => {
                                const sceneItem = findSceneItem(choice.act, choice.scene);
                                if (sceneItem) {
                                    const choiceText = sceneItem.querySelector('.choice-history');
                                    if (!choiceText) {
                                        const choiceSpan = document.createElement('span');
                                        choiceSpan.className = 'choice-history';
                                        choiceSpan.textContent = `: ${choice.choice_text}`;
                                        sceneItem.querySelector('.scene-text').appendChild(choiceSpan);
                                    }
                                }
                            });
                        }

                        // 只有在当前视图是当前场景时才显示
                        if (currentView === 'current') {
                            showCurrentScene();
                        }
                    })
                    .catch(error => {
                        console.error('Error fetching progress:', error);
                    });
            }

            // 根据幕和关卡查找对应的场景元素
            function findSceneItem(act, scene) {
                const sceneItems = document.querySelectorAll('.scene-item');
                for (const item of sceneItems) {
                    const itemAct = parseInt(item.getAttribute('data-act'));
                    const itemScene = parseInt(item.getAttribute('data-scene'));
                    if (itemAct === act && itemScene === scene) {
                        return item;
                    }
                }
                return null;
            }

            // 获取故事类型名称
            function getStoryType() {
                const storyType = storyTypeBadge.textContent;
                return storyType || '未知';
            }

            // 获取故事类型名称
            function getStoryTypeName(type) {
                const typeMap = {
                    'sci-fi': '科幻',
                    'apocalypse': '末世',
                    'fantasy': '奇幻'
                };
                return typeMap[type] || type;
            }

            // 显示加载模态框
            function showLoading() {
                loadingModal.style.display = 'flex';
            }

            // 隐藏加载模态框
            function hideLoading() {
                loadingModal.style.display = 'none';
                updateProgress(0);
            }

            // 更新进度条
            function updateProgress(percentage) {
                const progressBar = document.getElementById('progress-bar');
                if (progressBar) {
                    progressBar.style.width = percentage + '%';
                }
            }
        });

五、项目运行效果图

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐