# coming-future **Repository Path**: wangrenxuan/coming-future ## Basic Information - **Project Name**: coming-future - **Description**: 一款基于状态推移驱动的仙侠文字推理游戏,包含多结局与线索系统。 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-14 - **Last Updated**: 2026-08-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一、前往明天 - 文字推理游戏 > 一款基于 Spring Boot + MongoDB + Redis 开发的仙侠文字推理游戏,围绕"状态推移"驱动剧情分支与多结局。 > 点击试玩:[这里!]( http://47.84.121.154 ) > 图片资源已压缩至200KB且通过Nginx开启Gzip压缩+30天浏览器缓存,但由于项目部署在阿里云轻量服务器(新加坡节点),受跨境网络波动影响,国内部分用户首次加载可能仍有 2-10 秒延迟不等。项目体量较小,出于安全考虑,暂未接入OSS或CDN。 > 游戏背景图由豆包 AI 生成(非商用授权),人物立绘素材购自网络素材库。 > 安全说明:游戏不需要使用手机号/邮箱登录,也并不储存用户的IP地址。服务器仅允许密钥登录,不允许密码登录;数据库端口不向外暴露。用户账号的密码由密文储存,没有删除账号功能。由于目前处于mvp状态,设置了很低的每日注册限额;后续开发完成所有结局后,会放开限额并采取更多安全措施(目前,项目也处于无标签无宣传状态)。 --- ## 技术栈 | 技术 | 版本 | 用途 | |---|---|---| | Java | 17 (本地 17.0.18 / 服务器 17.0.19) | 开发语言 | | Spring Boot | 3.5.11 | 后端框架 | | MongoDB | 本地 8.0.13 / 服务器 7.0.37 | 数据存储 | | Redis | 服务器 7.0.15 | 注册限流 | | Nginx | 1.24.0 | 反向代理 + 静态资源缓存 | | Thymeleaf | 3.x (Spring Boot 默认) | 模板引擎 | | 操作系统 | Ubuntu 24.04.4 LTS (Noble) | 服务器运行环境 | ## 项目结构 ``` coming-future/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/coming_future/ │ │ ├── ComingFutureApplication.java # Spring Boot 启动类 │ │ │ │ │ ├── model/ # 数据模型层 │ │ │ ├── entity/ # 实体类(与 MongoDB 映射) │ │ │ │ ├── Player.java # 玩家实体(含 session 序列化) │ │ │ │ ├── Dialogue.java # 对话实体 │ │ │ │ ├── Introduction.java # 线索/人物/小报介绍实体 │ │ │ │ └── Sentence.java # 对话句子实体(label + text) │ │ │ │ │ │ │ └── enums/ # 枚举类(控制函数输入规范) │ │ │ ├── Role.java # 角色枚举 │ │ │ ├── Scene.java # 场景枚举 │ │ │ ├── InteractType.java # 交互类型枚举 │ │ │ └── IntroduceType.java # 介绍类型枚举 │ │ │ │ │ ├── repository/ # 数据访问层(MongoDB) │ │ │ ├── PlayerRepository.java # 玩家数据访问 │ │ │ ├── DialogueRepository.java # 对话数据访问 │ │ │ └── IntroductionRepository.java # 介绍数据访问 │ │ │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── PlayerService.java # 玩家服务(登录/注册/限流) │ │ │ └── DialogueService.java # 对话服务(核心剧情逻辑) │ │ │ │ │ ├── controller/ # 控制器层(前后端接口) │ │ │ ├── PlayerController.java # 玩家相关接口(登录/注册/面板) │ │ │ └── PageController.java # 页面路由(HTML 跳转) │ │ │ │ │ └── exception/ # 异常处理层 │ │ ├── GlobalExceptionHandler.java # 全局异常处理器 │ │ ├── UnauthorizedException.java # 未登录异常(401) │ │ ├── GameEndedException.java # 游戏已结案异常(403) │ │ ├── DialogueNotFound.java # 对话不存在异常(404) │ │ └── OnGameException.java # 游戏进行中异常(409) │ │ │ └── resources/ # 资源文件 │ ├── application.properties # Spring Boot 配置文件 │ │ │ ├── static/ # 静态资源 │ │ └── image/ # 游戏图片 │ │ │ └── templates/ # Thymeleaf 模板(HTML 页面) │ ├── test/ │ └── java/ │ └── com/example/coming_future/ │ ├── ExceptionTest.java # 异常类单元测试 │ └── FullGameFlowTest.java # 全流程集成测试 │ ├── pom.xml # Maven 依赖管理 └── README.md # 项目说明文档 └── DATA # 文件夹,包含部署教程和MongoDB中collection:dialogues和introductions的数据 ``` # 二、主要功能与状态推移逻辑 ## (一) ![状态推移逻辑](DATA/explain1.png) 玩家没有状态,状态分为**角色状态**和**场景状态**两种。状态内部,通过大部分无条件计数和少部分条件计数(被其他状态和其他计数制约增长,已设定计数上限,避免无限增长)来完成状态内部的剧情推进。新线索通过不同种类的对话触发加入,会影响两种状态。 玩家自带 `clue` 和 `flag` 的列表,玩家个人进度中,每个角色自带“提交过的”`clue` 的列表。角色通过 **角色 `clue` + 玩家 `flag`** 算出角色状态;场景通过 **玩家 `clue` + 玩家 `flag`** 算出场景状态。 ### service 对话调用思路(以角色为例) 每次对话调用流程如下: | 步骤 | 说明 | |------|------| | **0. 输入** | 玩家、交互角色、交互对话种类、具体选项(如某线索) | | **1. 初始化** | 查看该角色是否存在,不存在则将其加入可查看和提交的角色组 | | **2. 计数与线索** | 调用 `roleNumChange`,对此次对话调用行为进行条件计数(若原本没有,除了计数之外,也将影响角色对应的 `clue` 的 list),判断是否需要加线索 | | **3. 状态匹配** | 记录原 state,将角色现在的 `clue` 的 list 和玩家 `flag` 合并,若精准匹配到下一个 state 则调用 `set` 修改 state,否则保持原样 | | **4. 状态切换** | 取 state,若和原 state 不同,将此角色对应的 map 里的 `talk` / `clue` / `role` / `news` 四种计数全部清零,修改后的 player 存回数据库 | | **5. 对话输出** | 从 player 中取出此次对话调用行为对应的计数,从数据库中调用对应对话,输出对话或空 list | > 场景和场景可交互物品的对话调用类似,步骤更少。 **涉及 service 函数:** - `getRoleDialogue` - `getSceneDialogue` - `getIntroDialogue` ## (二)前端 ### 1. 对话类型区分 使用状态变量 `dialogueSource` 来区分当前对话对象: - 场景 - 角色 - 场景物品 在与角色对话中途,使用状态变量 `currentRole` 指示当前对话角色,初始为 `null`,对话结束后重新赋值为 `null`。 只有和角色对话中途,可以通过线索本对角色提交线索 / 人物 / 小报中的内容。 ### 2. 对话防护 在对话中途,若进入与当前状态变量 `dialogueSource` 不同种类的对话,**不能使用地图跳转和页面文字跳转**,在相应的 `script` 内加入防护逻辑。 ### 3. 对话调用方式 | 对话类型 | 调用时机 | |----------|----------| | **场景对话** | 载入场景、其他对话结束时调用 | | **角色对话** | 玩家点击时调用 | | **场景物品对话** | 玩家点击时调用 | ## (三)其他对话和显示功能 ### 1. `intro` 显示 在显示时做筛选,只显示: - 玩家对话过的角色 - 玩家已获得的状态 ### 2. 场景对话缓存 **问题:** 对话中途刷新或离开会导致对话丢失。对话的正常显示,需要“**先改状态,后调对话**”。如果玩家在对话中途刷新或离开页面,计数已经增加,下次再打开,计数再次增加,会调用下一段对话,而不是上次没播放完的对话。 **方案 A:Player 字段缓存(体验状态)** 需要在 player 的玩家状态之外,多加一个字段指示“玩家对话体验状态”。 例如:一个列表,其中存放自定义类的实例,字段包括缓存的“交互对象、交互方式、具体选项”。 可以不管上次未结束的交互是什么,只要再次和该对象交互,自动播放上次没结束的交互对话;也可以只有在重新进行同种交互同选项时才重新播放上次没结束的交互对话. - 每次前端玩家和对象进行交互,自动检索列表: - 如果没有关于此对象 / 此种交互的缓存 → 将信息加入,对话结束后清除缓存 - 如果有缓存 → 按照缓存指示的字段进行**无状态推移**的对话调用,调取上次未看完的对话 - 对话正常结束后,在对话结束的端口清除列表里的缓存 **问题:** 1. 对本项目这样体量的文字推理游戏来说,过度复杂,大材小用; 2. 可能让玩家已进行的操作在不知情的情况下失效。如果意外中断对体验影响不严重,强制播放缓存反而可能导致剧情体验混乱。 **方案 B:Session 缓存场景对话(最终采用)** - 在场景对话开始时,将场景名作为 `key` 设为 `true` 存入 `session` - 对话结束后设为 `false` - 若下次对话该场景对应的 `key` 为 `false` → 调用“会进行状态推移的调对话函数” `getSceneDialogue` - 若为 `true` → 调用“不会进行状态推移的调对话函数” `getSceneDialogue0` **效果:** 保证刷新页面和短时间离开后,对话体验连续;同时提供说明“最好不要对话中途离开页面”。 > 本项目最终采用了方案 B。 # 三、其他功能实现逻辑 ## 1. 登录、注册与全局注册限流 - 用户名和密码的规范校验、限流提示在注册失败时直接反馈给用户 - 登录成功后,玩家信息存入 `session`,获得会话标识 - 登录后进入面板页面,提供 **“新游戏”**、**“继续游戏”**、**“查看结局”** 三个入口 - “查看结局”同时展示已解锁结局与简易攻略 ## 2. 线索本发光提示 - 玩家实体字段: - `cluelight`:记录当前已查看的线索数量(数字) - `clueList`:玩家已获得的所有线索列表 - `DialogueService` 提供两个方法: - `clueLightOpen`:比较 `cluelight` 与 `clueList.size()`,返回是否应发光(`boolean`) - `clueLightClose`:将 `cluelight` 更新为当前 `clueList.size()`,使提示灯与持有线索数同步 - 接口调用时机: - 页面加载 / 对话结束 / 关闭线索本 → 调用 `clueLightOpen` 检查是否发光 - 打开线索本 → 调用 `clueLightClose` 熄灭提示灯 ## 3. 开始游戏与结案机制 - **`sceneStart`**:玩家离开起始场景后,自动获得 `flag: "START"` - **`sceneEnd`**:玩家点击“结案”时,删除 `"START"`,添加 `"END"` - **场景访问限制(`scenelimit` 系列)**: - 持有 `"START"` → 禁止进入起始场景 - 持有 `"END"` → 禁止进入除起始场景外的任何游戏场景 - 接口设计: - `"/end"` 单独暴露为结案接口,避免顺序问题,控制触发 - 其余逻辑(场景对话、session 管理、`scenelimit`)统一封装在 `"/scene"` 接口中 ## 4. 异常处理体系 覆盖四种核心异常场景: | 异常类型 | 触发条件 | |---|---| | `DialogueNotFound` | 对话数据缺失 | | `UnauthorizedException` | 未登录(无有效 session)进入游戏页面 | | `OnGameException` | 游戏已开始却返回起始场景 | | `GameEndedException` | 游戏已结束却试图进入游戏场景 | - 每种异常对应一个自定义异常类,由 `@RestControllerAdvice` 集中处理,给每个异常类分发状态码 - 每种异常返回不同的 HTTP 状态码(`401` / `403` / `404` / `409`) - 后端在需要的地方抛出对应异常类 - 前端通过在script加入全局 `fetch` 拦截器统一捕获响应: - 根据状态码弹出对应提示 - 跳转到对应页面(登录页 / 结算页 / 首页) - 页面初始化时可能触发多个请求(如场景对话 + 线索本灯光 + 人物和图片的条件显示),通过 `isErrorHandling` 布尔标志控制: - 第一次异常触发后置为 `true` - 防止同一页面加载过程中弹出多个重复错误提示 ## 5. 条件显示(角色 / 场景物品) - 需求简单,采用轻量方案: - `DialogueService` 提供 `decidePic` 方法,根据当前场景和玩家信息返回需显示的标识符(`String`) - 封装为 `/decidepic` 接口供前端调用 - 前端根据返回的字符串决定显示或隐藏对应元素: - 若返回 `"LIGUANSHI"` → 显示李管事 - 若返回 `"KENG"` → 显示坑 - 若返回 `null` → 全部隐藏 - 调用时机:页面加载完成 & 每段对话结束后 # 四、部署 ## (一)本地准备 1. 将项目克隆 / 下载到本地 2. 解压 `DATA/` 文件夹中的数据包 3. 在本地 MongoDB 中新建数据库 `coming_future` - 数据导入 collections:`dialogues`、`introductions` - 新建空 collection:`players` 4. 修改 `application.properties`: - 将内容替换为注释中的配置模板 - 按实际环境修改 MongoDB 连接信息 5. 若本地不需要 Redis 限流,删除 `PlayerService` 中的 Jedis 相关代码块 ## (二)服务器部署 > 本地Session中的玩家数据已实现序列化储存,部署详细步骤见 `DATA/部署.txt` **服务器环境:** 阿里云轻量应用服务器,Ubuntu 24.04 LTS,2 GiB 内存 **部署流程概览:** | 步骤 | 操作 | |------|------| | 1 | 开启 SSH 密钥登录(推荐) | | 2 | 登录服务器,执行系统更新 | | 3 | 安装 Java 17、MongoDB、Redis | | 4 | 为 MongoDB 和 Redis 设置密码 | | 5 | 上传本地 MongoDB 数据(`mongodump` / `mongorestore`) | | 6 | 上传 `jar` 包并启动服务 | | 7 | 配置 Nginx 反向代理 + 静态资源缓存 | ### 关于 Redis 连接方案说明 > 在本项目使用的 Spring Boot 版本(3.5.11)下,`spring-boot-starter-data-redis` 依赖提供的redis配置方案不能正常生效。版本兼容不存在问题,经排查,推测为连接方式的代码实现存在问题。 > 最终采用 **Jedis** 直连方案替代,Redis 服务在服务器上正常运行,限流功能可用。 ![redis运行图](DATA/explain2.png) ### 关于 Nginx缓存类型:协商缓存 ![304状态码](DATA/explain4.png) # 五、测试与规划 ## 测试 - **异常类单元测试** (`ExceptionTest.java`):覆盖四种自定义异常的状态码与跳转逻辑 - **全流程集成测试** (`FullGameFlowTest.java`):模拟玩家从登录→探索→结案/死亡的完整状态推移路径 ## 未来规划 | 阶段 | 目标 | |------|------| | **当前 MVP** | 单主线 + 5 个关键决策点 + 3 结局 | | **近期** | 补全第二条主线,避免单线死档 | | **中期** | 加入更多文字图片和动态效果,改善剧情和结局内容体验 | | **远期** | 开发后续剧情 | ### 关于测试:本地测试,使用MongoDB真实数据,每个@Test方法前后自动建立和清楚测试数据。 ![异常类单元测试成功截图](DATA/test2.png) ![全流程集成测试成功截图](DATA/test1.png)