交互式文本生成游戏
PathfinderTales是一款基于AI生成的互动文本冒险游戏,支持科幻、末世和奇幻三种故事类型。玩家通过选择不同选项来推动剧情发展,体验个性化叙事旅程。游戏采用Flask后端框架,结合SQLite数据库存储进度,前端使用响应式设计适配多设备。核心功能包括:AI动态生成剧情(通过DeepSeek API)、多分支故事线、进度自动保存、历史记录回顾等。技术栈涵盖Flask、SQLAlchemy、
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 + '%';
}
}
});
五、项目运行效果图
更多推荐
所有评论(0)