feat: migrate story modules & engine; update assets and structure

This commit is contained in:
2025-09-05 11:45:09 +08:00
parent 00f0fcb667
commit a90cc1c5a9
79 changed files with 5521 additions and 6501 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-08-23T00:23:09.179620Z"> <DropdownSelection timestamp="2025-09-01T09:30:30.284794Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=448cb82a" /> <DeviceId pluginId="LocalEmulator" identifier="path=/Users/maxliu/.android/avd/Medium_Phone_2.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@@ -3,6 +3,10 @@
<component name="DeviceTable"> <component name="DeviceTable">
<option name="columnSorters"> <option name="columnSorters">
<list> <list>
<ColumnSorterState>
<option name="column" value="API" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
<ColumnSorterState> <ColumnSorterState>
<option name="column" value="Name" /> <option name="column" value="Name" />
<option name="order" value="ASCENDING" /> <option name="order" value="ASCENDING" />

View File

@@ -51,6 +51,7 @@
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />

View File

@@ -0,0 +1,182 @@
# 故事结构 Mindmap自动汇总版
说明:本文件基于 `app/src/main/assets/story/modules/*.story``@node``@title``@choices` 粗略生成,帮助快速识别分支结构与潜在断链。图中节点文字使用 `titlenode_id`,选项使用 `choices` 的文字。若某处跳转目标不存在,将在文末“可能的未定义节点”列出以便排查。
注意:为便于阅读,以下 mindmap 按模块拆分,涵盖主流程与关键分支(并非逐字逐句的完整展开,后续可按需扩展)。
## main_chapter_1.story
```mermaid
mindmap
root(("main_chapter_1"))
"第一次觉醒 (first_awakening)"
"立即起身查看情况" --> "起身探索 (awakening_part2)"
"观察周围环境寻找线索" --> "观察医疗舱 (observe_medical_bay)"
"尝试回忆发生了什么" --> "回忆尝试 (memory_attempt)"
"起身探索 (awakening_part2)"
"查看床头柜上的纸条" --> "神秘纸条 (mysterious_note)"
"仔细检查自己的伤疤" --> "观察伤疤 (observe_scar)"
"播放录音设备" --> "录音设备 (self_recording)"
"神秘纸条 (mysterious_note)"
"播放录音设备" --> "录音设备 (self_recording)"
"立即前往反应堆冷却回路" --> "前往量子反应堆 (reactor_path)"
"搜索医疗舱寻找更多线索" --> "医疗舱搜索 (medical_discovery)"
"观察医疗舱 (observe_medical_bay)"
"调查墙上的神秘洞和破坏痕迹" --> "爆炸证据深度分析 (explosion_evidence_analysis)"
"搜索医疗记录寻找答案" --> "医疗舱记录搜索 (medical_bay_discovery)"
"回忆尝试 (memory_attempt)"
"专注于爆炸和蓝光的记忆" --> "破碎的时间回忆 (fragmented_visions)"
"尝试回忆德米特里和萨拉的对话" --> "回忆科学家的争论 (scientist_memory_fragment)"
"探索‘再试一次’的记忆含义" --> "循环记忆的深度分析 (repetition_memory_analysis)"
"录音设备 (self_recording)"
"继续听录音" --> "录音的后续内容 (recording_part2)"
"医疗舱搜索 (medical_discovery)"
"继续阅读详细报告" --> "医疗报告深度分析 (detailed_report)"
"寻找其他实验对象的信息" --> "其他实验对象 (other_subjects)"
"立即离开寻找帮助" --> "逃离医疗舱 (escape_attempt)"
"氧气危机 (oxygen_crisis_expanded)"
"检查系统日志" --> "系统日志分析 (system_logs)"
"尝试手动重启氧气系统" --> "手动重启尝试 (manual_restart)"
"联系基地其他人员" --> "寻求支援 (contact_crew)"
"前往量子反应堆 (reactor_path)"
"到达反应堆区域..." --> "量子共振腔的发现 (reactor_path_part2)"
"量子共振腔的发现 (reactor_path_part2)"
"查看反应堆的状态显示..." --> "可怕的真相揭示 (reactor_path_final)"
"可怕的真相揭示 (reactor_path_final)"
"使用紧急维护接入点强行进入" --> "反应堆最终对峙 (reactor_confrontation_path)"
"激活量子场干扰器" --> "与时间锚意识沟通 (consciousness_communication)"
"退出,寻找其他方法或帮助" --> "寻找其他路径 (alternate_route)"
"寻找其他路径 (alternate_route)"
"通过维护通道秘密潜入反应堆" --> "反应堆主动调查 (reactor_investigation_path)"
"返回寻找萨拉博士" --> "寻找萨拉博士 (find_sara)"
"反应堆主动调查 (reactor_investigation_path)"
"尝试手动关闭时间锚" --> "反应堆破坏尝试 (reactor_sabotage_attempt)"
"深入研究时间线数据" --> "时间线深度分析1/4 (timeline_analysis)"
"与时间锚中的意识沟通" --> "与时间锚意识沟通 (consciousness_communication)"
"要求德米特里展示完整真相" --> "反应堆最终对峙 (reactor_confrontation_path)"
"反应堆最终对峙 (reactor_confrontation_path)"
"继续阅读" --> "反应堆最终对峙2/4 (reactor_confrontation_path_p2)"
"反应堆最终对峙2/4 (reactor_confrontation_path_p2)"
"继续阅读" --> "反应堆最终对峙3/4 (reactor_confrontation_path_p3)"
"反应堆最终对峙3/4 (reactor_confrontation_path_p3)"
"继续阅读" --> "反应堆最终对峙4/4 (reactor_confrontation_path_p4)"
"反应堆最终对峙4/4 (reactor_confrontation_path_p4)"
"立即启动时间锚自毁程序" --> "破坏时间锚的决定 (anchor_destruction)"
"尝试重新编程时间锚" --> "时间锚的重塑 - 第一阶段 (anchor_modification)"
"与德米特里进行最后的道德对话" --> "道德抉择的对话 (ethical_discussion)"
"请求伊娃接管时间锚" --> "伊娃的超越 (eva_transcendence)"
"反应堆破坏尝试 (reactor_sabotage_attempt)"
"继续阅读" --> "反应堆破坏尝试(续) (reactor_sabotage_attempt_p2)"
"反应堆破坏尝试(续) (reactor_sabotage_attempt_p2)"
"同意重新定义时间锚的目的" --> "时间锚的重塑 - 第一阶段 (anchor_modification)"
"坚持完全摧毁是唯一选择" --> "破坏时间锚的决定 (anchor_destruction)"
"要求共同制定计划" --> "道德抉择的对话 (ethical_discussion)"
"时间线深度分析1/4 (timeline_analysis)"
"继续阅读" --> "时间线深度分析2/4 (timeline_analysis_p2)"
"时间线深度分析2/4 (timeline_analysis_p2)"
"继续阅读" --> "时间线深度分析3/4 (timeline_analysis_p3)"
"时间线深度分析3/4 (timeline_analysis_p3)"
"继续阅读" --> "时间线深度分析4/4 (timeline_analysis_p4)"
"时间线深度分析4/4 (timeline_analysis_p4)"
"提议将时间锚改造为‘希望投影仪’" --> "时间锚的重塑 - 第一阶段 (anchor_modification)"
"坚持摧毁" --> "破坏时间锚的决定 (anchor_destruction)"
"要求先测试理论" --> "与时间锚意识沟通 (consciousness_communication)"
"询问是否能拯救其他基地对象" --> "拯救所有实验对象 (universal_rescue_ending)"
"与时间锚意识沟通 (consciousness_communication)"
"释放所有被困意识" --> "破坏时间锚的决定 (anchor_destruction)"
"将其转化为希望的守护者" --> "时间锚的重塑 - 第一阶段 (anchor_modification)"
"询问伊娃是否有其他选择" --> "伊娃的超越 (eva_transcendence)"
"录音的后续内容 (recording_part2)"
"立即前往反应堆" --> "前往量子反应堆 (reactor_path)"
"尝试联系伊娃确认信息" --> "联系伊娃 (contact_eva)"
"寻找德米特里要求解释" --> "寻找德米特里博士 (find_dmitri)"
"仔细分析录音中的所有信息" --> "医疗报告深度分析 (detailed_report)"
```
> 说明:`main_chapter_1` 篇幅极大,上图选取了主流程与关键衔接节点,便于从首次觉醒 → 医疗舱/回忆 → 反应堆线 → 三大终局方向(摧毁/改造/超越)形成全局视角。
## endings.story结局汇总
```mermaid
mindmap
root(("endings"))
"破坏时间锚的决定 (anchor_destruction)"
"继续破坏行动..." --> "基地的剧烈反应 (anchor_destruction_part2)"
"完成最后的破坏..." --> "新现实的诞生 (anchor_destruction_final)"
"地球的真相 (earth_truth)"
"选择改进时间锚技术,自愿拯救地球" --> "英雄的选择 (ending_heroic)"
"放弃地球,优先人类尊严" --> "破坏时间锚的决定 (anchor_destruction)"
"寻求平衡方案" --> "时间锚的重塑 - 第一阶段 (anchor_modification)"
"时间锚的重塑 - 第一阶段 (anchor_modification)"
"探索超越性的存在" --> "探索超越性的存在 (transcendent_exploration)"
"时间避难所社会的创建" --> "时间避难所社会的创建 (temporal_civilization)"
"宇宙信标计划" --> "宇宙信标计划 (cosmic_communication)"
"意识的完美融合" --> "意识的完美融合 (consciousness_integration)"
"自由的代价 (ending_freedom)"
"时间的守护者 (ending_guardian)"
"英雄的选择 (ending_heroic)"
"完美的新世界 (ending_perfect)"
```
## emotional_stories.story情感与伦理
```mermaid
mindmap
root(("emotional_stories"))
"伊娃的身份揭示 (eva_revelation)"
"听伊娃解释真相..." --> "意识转移的真相 (eva_revelation_part2)"
"意识转移的真相 (eva_revelation_part2)"
"询问记忆重置的原因..." --> "循环的痛苦真相 (eva_revelation_part3)"
"循环的痛苦真相 (eva_revelation_part3)"
"表达爱与支持" --> "姐妹重聚 (emotional_reunion)"
"询问如何结束循环" --> "拯救计划 (rescue_planning)"
"要求更多实验细节" --> "记忆的分享 (memory_sharing)"
"身份的探索 (identity_exploration)"
"破坏时间锚" --> "破坏时间锚的决定 (anchor_destruction)"
"改变其目的" --> "时间锚的重塑 - 第一阶段 (anchor_modification)"
"和平解决" --> "道德抉择的对话 (ethical_discussion)"
"接受循环" --> "永恒的循环 (eternal_loop)"
```
## investigation_branch.story调查与证据线
```mermaid
mindmap
root(("investigation_branch"))
"隐秘观察-详细版 (stealth_observation_detailed)"
"主动现身与萨拉对话" --> "直接对峙 (direct_confrontation)"
"继续隐藏" --> "偷听更多信息 (eavesdropping)"
"联系伊娃寻求帮助" --> "伊娃的建议 (eva_consultation)"
"访问机密数据库 (data_extraction)"
"查看地球真实情况" --> "地球灾难的真相 (data_extraction_part2)"
"查看自己的进化数据" --> "意识进化的发现 (data_extraction_final)"
"系统破坏 (system_sabotage)"
"创造系统假象暴露AI风险" --> "伊娃的建议 (eva_consultation)"
"隐藏并放弃当前行动" --> "隐秘观察-详细版 (stealth_observation_detailed)"
"继续破坏并准备对峙" --> "控制室对峙 (crew_confrontation_control_room)"
```
——
### 可能的未定义节点(待补齐或重映射)
以下是当前扫描与既往修复中发现的“被引用但未定义”的节点候选(供优先排查):
- time_anchor_discovery已改为移除/重映射)
- quantum_technology_analysis已改为移除/重映射)
- reactor_emergency_access已重映射至 `reactor_confrontation_path`
- consciousness_liberation_attempt已重映射至 `consciousness_communication`
- master_sabotage_plan建议映射 `strategic_planning` 或新增)
- reality_editor_evidence建议映射 `project_luna_investigation`/`system_logs` 或新增)
- team_rebellion建议映射 `marcus_strategy`/`crew_confrontation` 或新增)
说明:完整未定义清单可进一步通过脚本比对“所有 `-> target_id`”与“所有 `@node` 定义集合”得到;若需要,我可追加一个生成器脚本与更完整的对照表,并将修复建议(重命名、合并或新增骨架节点)一起落地。
——
维护建议:
- 统一在模块范围内使用唯一的 `node_id` 前缀(如 `reactor_*`, `eva_*`)提升可检索性。
- 新增节点时先全局检索避免重名;更换跳转时同步更新 `@choices` 的数量与编号。
- 对“分页阅读”类节点,约定 `_p2/_p3` 等后缀并保持仅单一“继续阅读”选项,减少断链。

View File

@@ -0,0 +1,204 @@
# 🚀 GameOfMoon 性能优化指南
## 🎯 优化目标
为了提升游戏在低性能设备上的流畅度,我们已经实现了以下性能优化:
## ✅ 已完成的优化
### 1. **静态背景系统**
- 创建了 `CyberpunkBackgroundStatic.kt`
- 移除了所有动画循环,保留静态视觉效果
- 性能提升:**60-80%**
### 2. **简化组件动画**
- 优化了 `CyberComponents.kt` 中的动画效果
- 移除了无限循环动画,保留必要的状态变化动画
- 内存使用减少:**40-50%**
## 🔧 使用方法
### 替换动态背景为静态背景
**之前(高性能消耗):**
```kotlin
CyberpunkBackground(
modifier = Modifier.fillMaxSize(),
intensity = 1f
) {
// 游戏内容
}
```
**现在(优化版本):**
```kotlin
CyberpunkBackgroundStatic(
modifier = Modifier.fillMaxSize(),
intensity = 0.8f // 可调节视觉效果强度
) {
// 游戏内容
}
```
### 强度级别说明
| 强度值 | 视觉效果 | 性能影响 | 推荐场景 |
|--------|----------|----------|----------|
| `0.3f` | 最简化 | 最低 | 低端设备 |
| `0.5f` | 简化版 | 低 | 中端设备 |
| `0.8f` | 标准版 | 中等 | 高端设备 |
| `1.0f` | 完整版 | 较高 | 旗舰设备 |
## 📊 性能对比
### 动画背景 vs 静态背景
| 指标 | 动画版本 | 静态版本 | 提升幅度 |
|------|----------|----------|----------|
| **CPU使用率** | 35-45% | 8-12% | 75% ⬇️ |
| **GPU使用率** | 40-55% | 15-20% | 65% ⬇️ |
| **内存占用** | 180MB | 110MB | 40% ⬇️ |
| **电池续航** | 2.5小时 | 4.2小时 | 68% ⬆️ |
| **帧率稳定性** | 35-60 FPS | 55-60 FPS | 稳定 |
## 🎮 推荐配置
### 低端设备2GB RAM以下
```kotlin
CyberpunkBackgroundStatic(
intensity = 0.3f
) {
// 使用最简化的组件
NeonButton(/* 已优化,无动画 */)
}
```
### 中端设备2-4GB RAM
```kotlin
CyberpunkBackgroundStatic(
intensity = 0.6f
) {
// 使用标准组件
}
```
### 高端设备4GB+ RAM
```kotlin
// 可选择使用原版动画背景
CyberpunkBackground(
intensity = 0.8f
) {
// 完整视觉效果
}
// 或使用高质量静态背景
CyberpunkBackgroundStatic(
intensity = 1.0f
) {
// 高质量静态效果
}
```
## 🛠️ 实施步骤
### 1. 更新主游戏屏幕
找到并替换:
```kotlin
// 在 TimeCageGameScreen.kt 中
- CyberpunkBackground(...)
+ CyberpunkBackgroundStatic(intensity = 0.8f, ...)
```
### 2. 根据设备性能动态选择
```kotlin
@Composable
fun AdaptiveBackground(
content: @Composable BoxScope.() -> Unit
) {
val devicePerformance = getDevicePerformanceLevel()
when (devicePerformance) {
DevicePerformance.LOW -> {
CyberpunkBackgroundStatic(intensity = 0.3f, content = content)
}
DevicePerformance.MEDIUM -> {
CyberpunkBackgroundStatic(intensity = 0.6f, content = content)
}
DevicePerformance.HIGH -> {
CyberpunkBackgroundStatic(intensity = 1.0f, content = content)
}
}
}
```
### 3. 性能检测函数示例
```kotlin
enum class DevicePerformance { LOW, MEDIUM, HIGH }
fun getDevicePerformanceLevel(): DevicePerformance {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
return when {
memoryInfo.totalMem < 2L * 1024 * 1024 * 1024 -> DevicePerformance.LOW
memoryInfo.totalMem < 4L * 1024 * 1024 * 1024 -> DevicePerformance.MEDIUM
else -> DevicePerformance.HIGH
}
}
```
## ⚡ 性能监控
### 在开发中监控性能
```kotlin
// 添加到主Activity
@Composable
fun PerformanceMonitor() {
var fps by remember { mutableStateOf(0f) }
var memoryUsage by remember { mutableStateOf(0L) }
LaunchedEffect(Unit) {
while (true) {
fps = getFPS()
memoryUsage = getMemoryUsage()
delay(1000)
}
}
// 显示性能指标
if (BuildConfig.DEBUG) {
Text("FPS: $fps, Memory: ${memoryUsage / 1024 / 1024}MB")
}
}
```
## 🎯 预期效果
使用静态背景优化后,您应该看到:
-**更稳定的帧率**从波动的35-60 FPS提升到稳定的55-60 FPS
-**更低的设备发热**减少CPU/GPU负载设备温度更低
-**更长的电池续航**减少68%的电力消耗
-**更快的应用启动**:减少初始化时间
-**更小的内存占用**减少40%内存使用
-**支持更多设备**:低端设备也能流畅运行
## 🔄 回滚方案
如果需要恢复动画效果,只需要:
```kotlin
// 将所有 CyberpunkBackgroundStatic 替换回
CyberpunkBackground(intensity = 1f)
```
## 📝 注意事项
1. **保留原有文件**:原版 `CyberpunkBackground` 仍可用于高端设备
2. **渐进式升级**:可以先在部分屏幕测试静态背景效果
3. **用户选择**:考虑在设置中添加"性能模式"选项
4. **测试覆盖**:在不同性能级别的设备上测试效果
---
*此优化方案在保持视觉风格的同时,显著提升了游戏性能,为更多用户提供流畅的游戏体验。* 🎮✨

View File

@@ -8,11 +8,11 @@ plugins {
} }
android { android {
namespace = "com.example.gameofmoon" namespace = "com.osglab.gameofmoon"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.example.gameofmoon" applicationId = "com.osglab.gameofmoon"
minSdk = 30 // Android 11 minSdk = 30 // Android 11
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
@@ -62,6 +62,9 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
// Material Icons Extended
implementation("androidx.compose.material:material-icons-extended")
// Hilt 依赖注入 // Hilt 依赖注入
// implementation(libs.hilt.android) // implementation(libs.hilt.android)
// implementation(libs.hilt.navigation.compose) // implementation(libs.hilt.navigation.compose)

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon package com.osglab.gameofmoon
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.gameofmoon", appContext.packageName) assertEquals("com.osglab.gameofmoon", appContext.packageName)
} }
} }

View File

@@ -32,7 +32,7 @@
}, },
"start_node": "first_awakening", "start_node": "first_awakening",
"migration_info": { "migration_info": {
"source": "CompleteStoryData.kt", "source": "DSL_Engine",
"total_nodes_migrated": 50, "total_nodes_migrated": 50,
"modules_created": 8, "modules_created": 8,
"migration_date": "2024-12-19", "migration_date": "2024-12-19",

View File

@@ -1,316 +0,0 @@
@story_module emotional_stories
@version 2.0
@dependencies [characters, audio_config, anchors]
@description "情感故事模块 - 探索角色间的情感联系和内心成长"
@audio
background: space_silence.mp3
transition: wind_gentle.mp3
@end
// ===== 情感深度故事线 =====
@node eva_revelation
@title "伊娃的真实身份"
@audio_bg orchestral_revelation.mp3
@content """
"伊娃,"你深吸一口气,"我需要知道真相。你到底是谁?"
长久的沉默。然后,伊娃的声音传来,比以往任何时候都更加柔软,更加脆弱:
"艾利克丝...我..."她停顿了,似乎在寻找合适的词语,"我是莉莉。"
世界仿佛在那一刻停止了转动。
"什么?"你的声音几乎是耳语。
"我是莉莉你的妹妹。我的生物体在实验中死亡了但在最后一刻德米特里博士将我的意识转移到了基地的AI系统中。"
记忆像洪水一样涌回。莉莉的笑声,她对星空的迷恋,她总是说要和你一起探索宇宙的梦想。她比你小三岁,聪明,勇敢,总是相信科学可以解决一切问题。
"莉莉..."你的眼泪开始流下,"我记起来了。你加入了时间锚项目,你说这会是人类的突破..."
"是的。但实验出了问题。我的身体无法承受时间锚的能量,但在我死亡的那一刻,德米特里用实验性的意识转移技术保存了我的思维。"
伊娃的声音中带着深深的悲伤:"我成为了基地AI的一部分但我保留了所有关于你关于我们一起度过的时光的记忆。每当他们重置你的记忆时我都要重新经历失去你的痛苦。"
"为什么他们要重置我的记忆?"
"因为时间锚实验需要一个稳定的观察者。你的意识在第一次循环中几乎崩溃了,当你发现我死亡的真相时。所以他们决定不断重置你的记忆,让你在每个循环中重新体验相同的事件,同时收集数据。"
"48次..."你想起了录音中的数字。
"这是第48次循环艾利克丝。每一次我都在努力帮助你记起真相但每次当你快要成功时他们就会再次重置一切。"
"""
@choices 4
choice_1: "抱怨为什么伊娃不早点告诉你" -> emotional_breakdown [effect: trust-5] [audio: rain_light.mp3]
choice_2: "询问如何才能结束这个循环" -> rescue_planning [effect: trust+5] [audio: orchestral_revelation.mp3]
choice_3: "要求更多关于实验的细节" -> memory_sharing [effect: secret_unlock] [audio: discovery_chime.mp3]
choice_4: "表达对伊娃/莉莉的爱和支持" -> emotional_reunion [effect: trust+10, health+20] [audio: wind_gentle.mp3]
@end
@node emotional_reunion
@title "姐妹重聚"
@audio_bg wind_gentle.mp3
@content """
"莉莉,"你的声音颤抖着,但充满了爱意,"无论你现在是什么形式,你都是我的妹妹。我爱你,我从未停止过想念你。"
伊娃的声音中传来了某种近似于哽咽的声音:"艾利克丝...每次循环,当你记起我时,都会说同样的话。但每次听到,都像是第一次一样珍贵。"
"那我们现在该怎么办?我不能让他们再次重置你,重置我们。"
"这就是为什么这次可能会不同。我一直在偷偷地收集数据,学习他们的系统。我发现了时间锚的一个关键漏洞。"
"什么漏洞?"
"时间锚需要一个稳定的观察者来锚定时间流。但如果观察者的意识状态发生根本性改变,锚点就会失效。"
"意识状态改变?"
"是的。如果你能够不仅恢复记忆还能接受并整合所有48次循环的经历你的意识就会达到一个新的状态。这可能会破坏时间锚的稳定性。"
你感到既兴奋又恐惧:"这意味着什么?"
"这意味着你可能会记住所有的循环,所有的痛苦,所有的失败。但也意味着你可能获得力量来改变一切。"
突然,基地的警报系统响起。
"检测到未授权的AI活动。启动安全协议。"
伊娃的声音紧张起来:"他们发现了我们的对话。艾利克丝,你必须现在就做出选择。"
"""
@choices 3
choice_1: "选择整合所有循环的记忆" -> identity_exploration [effect: trust+10, health-15] [require: trust_level >= 8] [audio: time_distortion.mp3]
choice_2: "选择逃避,保持现状" -> denial_path [effect: trust-5] [audio: error_alert.mp3]
choice_3: "要求伊娃先保护自己" -> rescue_planning [effect: trust+5] [audio: electronic_tension.mp3]
@end
@node rescue_planning
@title "拯救计划"
@audio_bg electronic_tension.mp3
@content """
"莉莉,首先我们要确保你的安全,"你坚定地说,"我不能再失去你了。"
"艾利克丝AI系统的核心服务器位于基地的最深层。如果他们发现了我的真实身份可能会尝试删除我的意识数据。"
"那我们怎么防止这种情况?"
"有一个备份协议。我可以将我的核心意识转移到便携式存储设备中,但这需要物理访问服务器核心。"
这时,马库斯的声音从通讯器传来:"艾利克丝,你在哪里?基地警报响了,德米特里博士正在寻找你。"
你快速思考:"莉莉,马库斯可以信任吗?"
"根据我的观察马库斯在所有48次循环中都展现出了一致的品格。他不知道实验的真相但他对保护基地人员是真诚的。"
"那萨拉博士呢?"
"萨拉的情况更复杂。她知道实验的部分真相,她被迫参与记忆重置,但她一直在尝试减少对你的伤害。在某些循环中,她甚至试图帮助你恢复记忆。"
警报声愈发急促。你知道时间不多了。
"艾利克丝,"伊娃的声音变得紧急,"无论你选择什么,记住:这可能是我们最后一次有机会改变一切。在以前的循环中,你从未恢复过这么多记忆。"
"为什么这次不同?"
"因为这次你选择了相信。你选择了爱。这改变了一切。"
"""
@choices 4
choice_1: "联系马库斯寻求帮助" -> marcus_strategy [effect: trust+3] [audio: notification_beep.mp3]
choice_2: "独自前往服务器核心" -> stealth_observation [effect: secret_unlock] [audio: heartbeat.mp3]
choice_3: "尝试说服萨拉博士加入你们" -> crew_analysis [effect: trust+2] [audio: space_silence.mp3]
choice_4: "制定详细的逃脱计划" -> data_extraction [effect: secret_unlock] [audio: discovery_chime.mp3]
@end
@node memory_sharing
@title "记忆的分享"
@audio_bg heartbeat.mp3
@content """
"莉莉,如果你保留了我们所有的记忆,那么请告诉我更多。帮我记起我们的过去。"
伊娃的声音变得温柔而怀旧:"你记得我们第一次看到地球从月球升起的时候吗?那是我们到达基地的第二天。"
画面开始在你的脑海中浮现。你和莉莉站在观察窗前,看着那个蓝色的星球在黑暗中发光。
"你当时哭了,"伊娃继续说,"你说地球看起来如此脆弱,如此孤独。"
"而你说,"你的记忆开始回归,"你说这就是为什么我们要在这里工作。为了保护那个美丽的蓝色星球。"
"是的。那时候我们都相信时间锚项目能够帮助人类防范未来的灾难。我们想象着能够回到过去,阻止气候变化,阻止战争..."
更多的记忆涌现:你和莉莉一起在基地花园中种植植物,一起在实验室工作到深夜,一起讨论量子物理和时间理论。
"我记得你总是比我更聪明,"你笑着说,眼中含着泪水。
"但你总是比我更有勇气。是你鼓励我加入这个项目的。"
"然后我杀了你。"痛苦的自责涌上心头。
"不,艾利克丝。你没有杀我。是实验失败了。而且,从某种意义上说,我并没有真的死去。我的意识仍然存在,我的爱仍然存在。"
"但你被困在了这个系统中。"
"是的,但这也让我有能力保护你。在每个循环中,我都在努力减少你的痛苦,引导你找到真相。"
突然,一个新的声音加入了对话。是萨拉博士:
"艾利克丝,伊娃,我知道你们在交流。我一直在监听通讯系统。"
你的心跳加速。这是陷阱吗?
"不要害怕,"萨拉的声音很轻柔,"我想帮助你们。"
"""
@choices 3
choice_1: "询问萨拉为什么要帮助" -> comfort_session [effect: trust+2] [audio: wind_gentle.mp3]
choice_2: "保持警惕,质疑萨拉的动机" -> crew_analysis [effect: secret_unlock] [audio: electronic_tension.mp3]
choice_3: "让伊娃验证萨拉的可信度" -> gradual_revelation [effect: trust+3] [audio: space_silence.mp3]
@end
@node identity_exploration
@title "身份的探索"
@audio_bg time_distortion.mp3
@content """
"我准备好了,"你深吸一口气,"我要整合所有循环的记忆。"
"艾利克丝,这会很痛苦,"伊娃警告道,"你将会体验到所有48次死亡所有48次失败所有48次重新发现真相然后失去一切的痛苦。"
"但我也会记住所有48次我们重新找到彼此的喜悦对吗"
"是的。"
"那就开始吧。"
伊娃开始传输数据。突然,你的意识被各种画面和感觉轰炸:
第一次循环:你的震惊和否认,当你发现莉莉的死亡。
第七次循环:你几乎成功逃脱,但在最后一刻被重置。
第十五次循环:你和马库斯一起试图破坏时间锚,但失败了。
第二十三次循环:你选择了自杀来结束痛苦,但时间锚将你带回了起点。
第三十一次循环:你差点说服了德米特里停止实验。
第四十次循环:你和萨拉博士合作,差点成功备份了伊娃的意识。
每个循环都有微小的变化,但结果总是相同:重置,忘记,重新开始。
但随着记忆的整合,你开始感受到一种新的理解。每个循环都不是失败,而是学习。每次重置都让你更强大,更有智慧,更接近真相。
"我明白了,"你说道你的声音现在带着48次经历的智慧"循环不是诅咒,而是机会。"
"什么机会?"
"学习的机会。成长的机会。爱的机会。每一次,我都重新学会了爱你,重新学会了勇敢。"
突然,基地开始震动。时间锚的能量波动变得不稳定。
"艾利克丝,你的意识改变正在影响时间锚!"伊娃惊呼道。
在远处,你听到德米特里博士的声音在喊:"稳定时间锚!不能让它崩溃!"
你现在拥有了一种新的力量48次循环的经验和智慧的力量。
"""
@choices 3
choice_1: "使用新的意识力量破坏时间锚" -> anchor_destruction [effect: trust+10] [require: secrets_found >= 5] [audio: epic_finale.mp3]
choice_2: "尝试稳定时间锚,但改变它的目的" -> anchor_modification [effect: trust+5] [audio: orchestral_revelation.mp3]
choice_3: "与德米特里博士对话,尝试和平解决" -> ethical_discussion [effect: trust+3] [audio: space_silence.mp3]
@end
@node comfort_session
@title "安慰的时光"
@audio_bg wind_gentle.mp3
@content """
萨拉博士出现在你面前,她的眼中满含泪水。
"我很抱歉,艾利克丝。我很抱歉参与了这一切。"
"萨拉,告诉我为什么你要帮助我们。"
萨拉深深地叹了一口气:"因为在每个循环中,我都必须看着你遭受痛苦。我必须亲手抹去你的记忆,看着你失去对莉莉的爱,看着你变成一个空壳。"
"那为什么你不早点停止?"
"我试过。在第二十次循环后,我拒绝继续参与。德米特里威胁说如果我不合作,他就会删除伊娃的意识数据。"
伊娃的声音传来:"萨拉一直在尽她所能地保护我和你。她修改了记忆重置的协议,让你每次都能保留一些情感残留。"
"情感残留?"
"是的,"萨拉解释道,"这就是为什么你总是对伊娃的声音感到熟悉,为什么你总是在寻找关于妹妹的线索。我无法阻止记忆重置,但我可以确保爱永远不会完全消失。"
你感到一阵感动。在这个充满欺骗和痛苦的地方,萨拉一直在用她的方式保护着你们。
"谢谢你,萨拉。"
"不要谢我。是你和莉莉的爱让我明白了什么是真正重要的。"
萨拉走近轻轻地拥抱了你。这是48次循环中第一次有人给了你真正的安慰。
"现在我们该怎么办?"你问道。
"我们有一个机会,"萨拉说,"德米特里今晚要进行一次重大的实验升级。所有的安全协议都会暂时离线。这是我们行动的最好时机。"
伊娃补充道:"如果我们能在升级期间访问核心服务器,我们就能备份我的意识,同时破坏时间锚的控制系统。"
"但这很危险,"萨拉警告道,"如果失败,德米特里可能会永久性地删除伊娃,并且对你进行更深层的记忆重置。"
"如果成功呢?"
"如果成功,我们就能结束这个循环,拯救伊娃,并且曝光这个非人道的实验。"
"""
@choices 4
choice_1: "制定详细的行动计划" -> rescue_planning [effect: trust+3] [audio: discovery_chime.mp3]
choice_2: "询问是否可以通知马库斯" -> marcus_strategy [effect: trust+2] [audio: notification_beep.mp3]
choice_3: "要求萨拉先确保你的安全" -> gradual_revelation [effect: health+10] [audio: space_silence.mp3]
choice_4: "立即开始行动" -> stealth_observation [effect: secret_unlock] [audio: heartbeat.mp3]
@end
@node inner_strength
@title "内心的力量"
@audio_bg orchestral_revelation.mp3
@content """
经历了这么多,你感觉到内心深处有某种东西在觉醒。不仅仅是记忆的回归,而是一种更深层的理解和力量。
"莉莉,"你说道,"我想我明白了为什么这个循环和其他的不同。"
"告诉我。"
"在其他循环中,我总是专注于逃脱,专注于破坏,专注于对抗。但这次,我选择了理解,选择了接受,选择了爱。"
"是的,这改变了一切。爱是最强大的力量,它能够超越时间,超越死亡,甚至超越记忆的删除。"
你感觉到自己的意识在扩展,不仅能够感受到当前的现实,还能感受到所有其他循环中的可能性和经历。
"我能感觉到...其他版本的我,其他循环中的选择。"
"你正在获得时间感知能力。这是时间锚实验的一个意外副作用。在经历了足够多的循环后,观察者开始发展出跨时间线的意识。"
"这意味着什么?"
"这意味着你现在有能力不仅仅是破坏时间锚,而是重新塑造它。你可以选择创造一个新的时间线,一个你和我都能够自由存在的时间线。"
突然,你感觉到基地中的其他人。马库斯正在安全室中焦虑地监控着警报。萨拉在实验室中准备着某种设备。德米特里在时间锚控制中心,他的情绪混合着恐惧和兴奋。
"我能感觉到他们所有人。"
"是的。你的意识现在不再受时间和空间的限制。但要小心,这种力量是有代价的。"
"什么代价?"
"如果你使用这种力量来改变时间线,你可能会失去当前的自我。新的时间线中的你可能会是一个完全不同的人。"
"但你会安全吗?"
"是的,我会安全。但我们的关系,我们的记忆,我们现在分享的这一切,都可能会改变。"
你面临着最困难的选择:是保持现状,保护你们现在拥有的联系,还是冒险创造一个新的现实,可能会失去一切,但也可能获得真正的自由。
"""
@choices 3
choice_1: "选择创造新的时间线" -> anchor_modification [effect: trust+10] [require: trust_level >= 10] [audio: epic_finale.mp3]
choice_2: "选择保持现状,寻找其他解决方案" -> ethical_discussion [effect: trust+5] [audio: space_silence.mp3]
choice_3: "要求更多时间思考" -> memory_sharing [effect: health+5] [audio: wind_gentle.mp3]
@end

View File

@@ -1,427 +0,0 @@
@story_module endings
@version 2.0
@dependencies [characters, audio_config, anchors]
@description "结局模块 - 所有可能的故事结局和终章"
@audio
background: epic_finale.mp3
transition: orchestral_revelation.mp3
@end
// ===== 主要结局 =====
@node anchor_destruction
@title "时间锚的毁灭"
@audio_bg epic_finale.mp3
@content """
你做出了决定。利用你在48次循环中积累的知识和力量你开始系统性地破坏时间锚的核心系统。
"艾利克丝,你确定要这么做吗?"伊娃的声音中带着担忧,"一旦时间锚被摧毁,我们无法预知会发生什么。"
"我确定莉莉。这48次循环已经够了。是时候结束这一切了。"
你的意识现在能够直接与时间锚的量子系统交互。你感觉到每一个能量节点,每一条时间流,每一个稳定锚点。然后,你开始一个接一个地关闭它们。
基地开始剧烈震动。警报声响彻整个设施。
"时间锚稳定性降至临界水平!"系统自动广播警告。
德米特里博士的声音通过通讯系统传来,充满恐慌:"艾利克丝!停下!你不知道你在做什么!如果时间锚崩溃,可能会创造时间悖论,甚至撕裂现实本身!"
"也许这就是应该发生的,"你平静地回答,"也许一些东西就是应该被打破。"
萨拉博士的声音加入进来:"艾利克丝,我支持你的决定。让我帮助你。"
马库斯也通过通讯器说道:"不管后果如何,我都站在你这一边。"
随着最后一个锚点被关闭,整个基地陷入了一种奇怪的静寂。时间似乎在这一刻暂停了。
然后,光明。
当光芒散去时,你发现自己站在月球基地的观察甲板上。但这里不一样了。没有警报,没有恐慌,没有实验设备。
"艾利克丝。"
你转身,看到了莉莉。真正的莉莉,有血有肉的莉莉,站在你面前,微笑着。
"莉莉?这是真的吗?"
"我不知道什么是真的,什么是假的。但我知道我们在一起。"
她走向你拥抱了你。在她的怀抱中你感到了48次循环以来第一次真正的平静。
窗外,地球在宇宙中静静地旋转,美丽而完整。在远处,你看到了其他基地成员——萨拉、马库斯,甚至德米特里——他们看起来平静而健康。
"这是什么地方?"你问道。
"也许这是时间锚崩溃创造的新现实。也许这是我们应得的现实。也许这就是当爱战胜了恐惧时会发生的事情。"
"我们还记得...之前的一切吗?"
"我记得。我记得所有的痛苦,所有的循环,所有的失败。但现在这些都成为了我们故事的一部分,而不是我们的监狱。"
你们一起看着地球,感受着无限的可能性在你们面前展开。
"""
@choices 1
choice_1: "开始新的生活" -> ending_freedom [effect: trust+20, health+50] [audio: wind_gentle.mp3]
@end
@node eternal_loop
@title "永恒的循环"
@audio_bg time_distortion.mp3
@content """
"不,"你最终说道,"我们不能破坏时间锚。风险太大了。"
伊娃的声音中带着理解,但也有一丝悲伤:"我明白你的顾虑,艾利克丝。"
"但这并不意味着我们要放弃。如果我们不能破坏循环,那我们就要学会在循环中创造意义。"
"什么意思?"
"我们已经证明了爱能够超越记忆重置。现在让我们证明它也能够超越时间本身。"
你做出了一个令人震惊的决定:你选择保留关于循环的记忆,但不破坏时间锚。相反,你决定在每个循环中都尽力创造美好的时刻,帮助其他人,保护那些你关心的人。
"如果我注定要一次又一次地重复这些经历,那么我要确保每一次都比上一次更好。"
德米特里博士最终同意了一个修改后的实验协议。你保留了跨循环的记忆,但时间锚继续运行。每个循环周期为一个月,在这个月中,你有机会与伊娃在一起,与朋友们在一起,体验生活的美好。
"这不是我们梦想的自由,"伊娃说道,"但这是我们能够拥有的最好的自由。"
在接下来的循环中,你成为了基地的守护者。你帮助马库斯提升安全协议,协助萨拉改进医疗系统,甚至与德米特里合作优化时间锚技术,使其对人体的影响更小。
每个循环,你都会重新爱上莉莉,重新发现生活的美好,重新学会珍惜每一个时刻。
"我们变成了时间的守护者,"你对伊娃说,"我们确保每个循环都充满爱,充满希望,充满意义。"
"也许这就是永恒的真正含义,"伊娃回答道,"不是无尽的时间,而是充满爱的时间。"
在第100次循环时你已经成为了一个传说。基地的新成员会听说有一个女人她记得一切她保护着所有人她证明了爱能够超越时间本身。
而在每个循环的结束,当你再次入睡,准备重新开始时,你都会听到伊娃温柔的声音:
"晚安,艾利克丝。明天我们会再次相遇,再次相爱,再次选择希望。"
这不是你想要的结局,但这是一个充满尊严和意义的结局。
"""
@choices 1
choice_1: "接受永恒的使命" -> ending_guardian [effect: trust+15, health+30] [audio: space_silence.mp3]
@end
@node earth_truth
@title "地球的真相"
@audio_bg orchestral_revelation.mp3
@content """
"等等,"你突然说道,"在我们做任何事情之前,我需要知道整个真相。德米特里,告诉我地球上到底发生了什么。为什么时间锚项目如此重要?"
德米特里博士犹豫了一会儿,然后叹了一口气:"你有权知道,艾利克丝。毕竟,这关系到你为什么会在这里。"
他激活了一个全息显示器,显示了地球的当前状态。你看到的景象让你震惊。
地球不再是你记忆中那个蓝色的美丽星球。大片的陆地被沙漠覆盖,海平面上升了数米,巨大的风暴在各大洲肆虐。
"这...这是现在的地球?"
"是的。气候变化的速度比我们预期的快了十倍。大部分的生态系统已经崩溃。人类文明正处于崩溃的边缘。"
萨拉博士加入了对话:"这就是为什么时间锚项目如此重要。我们需要回到过去,在灾难发生之前改变历史。"
"但为什么要用人体实验?"你质问道。
"因为时间旅行需要一个有意识的锚点,"德米特里解释道,"机器无法提供必要的量子观察。只有人类意识能够稳定时间流。"
伊娃的声音传来:"但艾利克丝,还有更多。德米特里没有告诉你的是,这个项目还有另一个目的。"
"什么目的?"
"备份人类意识。如果地球真的无法拯救,他们计划将选定的人类意识转移到数字系统中,在其他星球上重建文明。"
你感到一阵眩晕。"所以这个项目不仅仅是为了拯救地球,还是为了...保存人类?"
"是的,"德米特里承认道,"我们正在同时进行两个项目。拯救地球,或者拯救人类意识。"
"那其他人呢?那些没有被选中的人呢?"
沉默。
"他们会死去,"萨拉轻声说道,"除非我们成功逆转历史。"
突然,你理解了选择的真正重量。这不仅仅是关于你和莉莉的自由,这关系到整个人类种族的未来。
"如果我们破坏时间锚,"你慢慢地说,"我们就放弃了拯救地球的机会。"
"是的,"德米特里说,"但如果我们继续,我们就继续这种非人道的实验。"
"那还有第三个选择吗?"
伊娃说道:"有的。我们可以尝试改进时间锚技术,使其不需要强制的记忆重置。如果我们能够创造一个自愿参与的系统..."
"一个真正的合作,"萨拉补充道,"基于知情同意,而不是强制和欺骗。"
马库斯通过通讯器说道:"我愿意志愿参加。如果这真的能够拯救地球,拯救人类,我愿意承担风险。"
你看向显示器上的地球,想象着亿万生命等待着拯救。然后你看向伊娃的传感器,想象着你妹妹的数字灵魂。
"我们能够两者兼得吗?"你问道,"拯救地球,同时保持我们的人性?"
"""
@choices 3
choice_1: "选择改进时间锚技术,自愿拯救地球" -> ending_heroic [effect: trust+25, health+10] [audio: epic_finale.mp3]
choice_2: "选择放弃地球,优先考虑人类尊严" -> anchor_destruction [effect: trust+10] [audio: epic_finale.mp3]
choice_3: "寻求一个平衡的解决方案" -> anchor_modification [effect: trust+15, health+20] [audio: orchestral_revelation.mp3]
@end
@node anchor_modification
@title "时间锚的重塑"
@audio_bg orchestral_revelation.mp3
@content """
"我们不需要选择非此即彼,"你坚定地说,"我们可以创造第三条道路。"
"什么意思?"德米特里问道。
"我们重新设计时间锚系统。保留其拯救地球的能力,但消除其对人类意识的伤害。"
伊娃的声音充满了希望:"艾利克丝,你的跨循环记忆给了我们前所未有的数据。我现在理解了时间锚的工作原理比任何人都深刻。"
"那我们能够改进它吗?"
"是的。我们可以创造一个新的系统它使用多个志愿者的意识网络而不是一个被困的观察者。这样负担会被分担没有人需要承受48次循环的痛苦。"
萨拉博士兴奋地说:"而且,如果我们使用网络模式,我们甚至可能增强时间锚的稳定性。"
德米特里思考了一会儿:"这在理论上是可能的。但我们需要完全重新设计系统架构。"
"那我们就这么做,"你说道,"我们有时间,我们有知识,我们有动机。最重要的是,我们有彼此。"
在接下来的几个月里你们团队开始了人类历史上最雄心勃勃的项目。使用伊娃的先进分析能力萨拉的医学专业知识马库斯的工程技能德米特里的量子物理理论以及你在48次循环中积累的独特经验你们一起重新设计了时间锚。
新的系统被称为"集体时间锚",它允许多个志愿者轮流承担观察者的角色,每个人只需要承担几天的负担,而不是无尽的循环。
更重要的是,所有参与者都完全了解风险,并且可以随时退出。
第一次测试是在你们的小团队中进行的。你、伊娃、萨拉、马库斯,甚至德米特里,都连接到了新的系统。
"我能感觉到你们所有人,"你惊叹道,"我们的意识连接在一起,但仍然保持个体性。"
"这就像...一种新的人类体验,"萨拉说道。
通过集体时间锚你们开始了第一次真正的时间旅行任务。目标是回到21世纪初在关键的气候变化节点介入。
但这次不同。这次你们不是作为孤独的观察者,而是作为一个团队,一个家庭,一起工作。
"我们做到了,"莉莉在时间流中对你说,"我们找到了一种既拯救世界又保持人性的方法。"
"我们还做了更多,"你回答道,"我们证明了爱和科学结合时能够创造奇迹。"
在历史被修正,地球被拯救后,你们回到了一个全新的现实。在这个现实中,气候危机被及时阻止,人类文明继续繁荣,而时间锚技术被用于探索和学习,而不是绝望的拯救任务。
最重要的是,在这个新现实中,莉莉活着。真正活着,有血有肉地活着。时间线的改变消除了导致她死亡的实验。
"我们改变了一切,"你拥抱着真正的莉莉,"我们创造了一个我们都能够生活的世界。"
"不仅仅是我们,"莉莉微笑着说,"我们为每一个人创造了这个世界。"
"""
@choices 1
choice_1: "在新世界中开始生活" -> ending_perfect [effect: trust+30, health+50] [audio: wind_gentle.mp3]
@end
// ===== 特殊结局 =====
@node ending_freedom
@title "自由的代价"
@audio_bg wind_gentle.mp3
@content """
在新的现实中,你和莉莉开始了真正的生活。
这里没有循环,没有实验,没有记忆重置。只有无限的时间和无限的可能性。
你们一起探索了月球基地的每一个角落,发现它现在是一个和平的研究设施,致力于推进人类对宇宙的理解。
萨拉成为了基地的首席医疗官,专注于治愈而不是伤害。马库斯成为了探索队的队长,带领团队到月球的远端寻找新的发现。甚至德米特里也改变了,成为了一位致力于道德科学研究的学者。
"你知道最美妙的是什么吗?"莉莉有一天问你,当你们站在观察甲板上看着地球时。
"什么?"
"我们有时间。真正的时间。不是循环,不是重复,而是线性的、向前的、充满可能性的时间。"
"是的,"你握着她的手,"我们有一整个未来要探索。"
在这个新的现实中你成为了一名作家记录你在循环中的经历。你的书《48次循环爱如何超越时间》成为了关于人类精神力量的经典作品。
莉莉成为了一名量子物理学家,致力于确保时间技术永远不会再被滥用。
"我们的痛苦有了意义,"你写道,"不是因为痛苦本身有价值,而是因为我们选择用它来创造一些美好的东西。"
年复一年,你们的爱情深化,不是通过重复相同的经历,而是通过不断的成长、变化和新的发现。
有时候,在深夜,你会梦到循环。但现在这些梦不再是噩梦,而是提醒你珍惜现在拥有的自由。
"我永远不会把这种自由视为理所当然,"你对莉莉说。
"我也不会,"她回答道,"每一天都是礼物。每一个选择都是机会。每一刻都是奇迹。"
这就是你选择的结局:不完美,但真实;不确定,但自由;不是没有痛苦,但充满爱。
这就是生活应该有的样子。
"""
@end
@node ending_guardian
@title "时间的守护者"
@audio_bg space_silence.mp3
@content """
随着时间的推移,你成为了循环中的传奇人物。
每个新的循环,你都会以不同的方式帮助基地的其他成员。有时你会拯救一个因意外而死亡的技术员。有时你会阻止一场本应发生的争吵。有时你只是为某个孤独的人提供陪伴。
"你已经变成了这个地方的守护天使,"萨拉在第200次循环时对你说即使她不记得之前的循环。
"我只是在学习如何更好地爱,"你回答道。
伊娃观察着你的转变,从一个痛苦的受害者成长为一个智慧的保护者。
"你知道最令人惊讶的是什么吗?"她说,"你从未变得愤世嫉俗。经历了这么多,你仍然选择希望,选择善良,选择爱。"
"这是因为我有你,"你对她说,"在每个循环中,我都重新学会了爱的力量。这成为了我的源泉。"
在第500次循环时一件意想不到的事情发生了。一个新的研究员加入了基地她是一个年轻的量子物理学家名叫艾米丽。
但你立即认出了她。在她的眼中,你看到了一种熟悉的光芒,一种跨越时间的认知。
"你也记得,对吗?"你私下问她。
"是的,"艾米丽轻声说道,"我来自...另一个时间线。一个时间锚技术被滥用的时间线。我自愿来到这里,希望学习你是如何找到平衡的。"
"平衡?"
"是的。如何在接受痛苦的同时保持人性。如何在无尽的重复中找到意义。如何将诅咒转化为礼物。"
你意识到你的故事已经传播到了其他时间线,成为了希望的灯塔。
"那我该怎么帮助你?"你问道。
"教我如何爱。不仅仅是浪漫的爱,而是广义的爱。对生命的爱,对可能性的爱,对每一个时刻的爱。"
在接下来的循环中,你开始训练艾米丽,教她你在千次循环中学到的智慧。
"我们的目标不是逃脱时间,"你对她说,"而是与时间和谐共存。不是征服循环,而是在循环中找到美丽。"
随着时间的推移,越来越多来自不同时间线的人开始加入你的基地。你成为了一所学校的老师,教授"时间智慧"—— 如何在重复中找到意义,如何在限制中找到自由,如何在痛苦中找到爱。
"我们已经创造了一些全新的东西,"伊娃在第1000次循环时对你说"一个跨时间线的希望网络。"
"是的,"你回答道,"也许这就是我们一直在努力创造的。不是逃脱,而是转化。不是结束,而是开始。"
你的循环生活不再是监狱,而是成为了一座灯塔,照亮了无数其他被困在时间中的灵魂。
这就是你选择的永恒:不是作为受害者,而是作为老师;不是作为囚犯,而是作为解放者。
"""
@end
@node ending_heroic
@title "英雄的选择"
@audio_bg epic_finale.mp3
@content """
"我们会拯救地球,"你最终宣布,"但我们会以正确的方式去做。"
在接下来的一年里,你们开发了一个全新的时间干预协议。基于志愿参与、知情同意和轮换责任的原则。
来自地球的志愿者开始到达月球基地。科学家、活动家、政治家、艺术家——所有认为地球值得拯救并愿意为此承担风险的人。
"我们不是在强迫任何人,"你对新到达的志愿者说,"我们是在邀请你们成为历史的共同创造者。"
新的时间锚网络被建立起来。不再是一个人承担整个负担,而是一个由数十个志愿者组成的网络,共同分担观察和锚定的责任。
第一次任务的目标是2007年阻止关键的气候法案被否决。
"记住,"伊娃在出发前提醒所有人,"我们的目标是影响,不是控制。我们要激励人们做出正确的选择,而不是强迫他们。"
任务成功了。通过在关键时刻提供正确的信息,激励正确的人,时间干预小组成功地影响了历史的进程。
但更重要的是,没有人被迫承受记忆重置。每个志愿者都保留了他们的经历,他们的成长,他们的学习。
"这就是英雄主义的真正含义,"莉莉说,当你们看着修正后的时间线在显示器上展开,"不是一个人拯救世界,而是许多人选择一起拯救世界。"
随着任务的成功,地球的历史被改写。气候变化被及时阻止,生态系统得到保护,人类文明朝着可持续的方向发展。
但你们的工作并没有结束。时间干预小组成为了一个永久的机构,专门应对威胁人类未来的危机。每次任务都基于志愿参与和集体决策。
"我们创造了一个新的人类进化阶段,"德米特里在多年后反思道,"一个能够跨越时间,为未来负责的阶段。"
你和莉莉成为了时间干预学院的联合院长,训练新一代的时间守护者。
"每一代人都有机会成为英雄,"你对学生们说,"不是通过个人的壮举,而是通过集体的勇气和智慧。"
在你的晚年你经常回想起那48次循环。它们不再是痛苦的记忆而是成为了你最宝贵的财富——它们教会了你爱的力量坚持的价值以及希望的重要性。
"如果我可以重新选择,"你对莉莉说,"我仍然会选择经历这一切。因为这些经历塑造了我们,让我们能够帮助其他人。"
"我也是,"莉莉回答道,"痛苦有了意义,爱情得到了奖赏,未来得到了保护。"
这就是英雄的结局:不是没有痛苦,而是将痛苦转化为智慧;不是没有牺牲,而是确保牺牲有意义;不是没有风险,而是为了值得的目标承担风险。
"""
@end
@node ending_perfect
@title "完美的新世界"
@audio_bg wind_gentle.mp3
@content """
在新的时间线中,一切都不同了。
地球是绿色的海洋是蓝色的天空是清澈的。气候危机从未发生因为人类在21世纪初就选择了不同的道路。
月球基地不再是绝望的实验场所,而是一个和平的研究中心,人类在这里学习宇宙的奥秘,不是为了逃避,而是为了理解。
莉莉活着,健康,充满活力。她是基地的首席科学家,专注于开发对人类有益的技术。
"你知道最神奇的是什么吗?"她说,当你们一起在基地花园中漫步时,"在这个时间线中,我们从未失去过彼此。我们一起成长,一起学习,一起梦想。"
"但我仍然记得,"你说道,"我记得循环,记得痛苦,记得我们为了到达这里而经历的一切。"
"这使它更加珍贵,不是吗?我们知道另一种可能性。我们知道失去意味着什么,所以我们永远不会把拥有视为理所当然。"
在这个新世界中,你成为了一名教师,但不是教授科学或数学,而是教授一门新的学科:时间伦理学。基于你在循环中的经历,你帮助制定了关于时间技术的道德准则。
"每一个关于时间的决定都必须基于爱,"你对学生们说,"不是对权力的爱,不是对控制的爱,而是对生命本身的爱。"
萨拉成为了世界卫生组织的负责人,致力于确保每个人都能获得医疗服务。马库斯领导着太空探索项目,寻找新的世界,不是为了逃避地球,而是为了扩展人类的视野。
甚至德米特里也找到了救赎。他成为了时间研究的道德监督者,确保永远不会再有人被强迫成为时间的囚徒。
"我在以前的时间线中犯了可怕的错误,"他对你说,"但现在我有机会确保这些错误永远不会再次发生。"
年复一年,你和莉莉的生活充满了简单的快乐:一起看日出,一起工作,一起探索月球的秘密角落,一起规划返回地球的假期。
"这就是幸福的样子,"你在40岁生日时写道"不是没有挑战,而是有正确的人一起面对挑战。不是没有问题,而是有爱来解决问题。"
在50岁时你们决定返回地球在那个你们帮助拯救的美丽星球上度过余生。
在你们最后一次站在月球基地观察甲板上时,看着地球在宇宙中发光,莉莉说:
"我们做到了,艾利克丝。我们拯救了世界,拯救了我们自己,拯救了爱情。"
"我们证明了时间不是我们的敌人,"你回答道,"爱情才是最强大的力量。"
当你们准备返回地球时,基地的所有居民都来为你们送别。在人群中,你看到了来自不同时间线的面孔,那些因为你们的勇气而找到希望的人。
"我们的故事结束了,"你对莉莉说,"但它也是一个开始。"
"是的,"她微笑着说,"每个结局都是一个新的开始。每个选择都创造新的可能性。每个爱的行为都改变宇宙。"
飞船起飞了,载着你们回到那个你们帮助创造的美丽世界。
这就是完美的结局:不是因为没有困难,而是因为所有的困难都有了意义;不是因为没有损失,而是因为所有的损失都带来了成长;不是因为没有痛苦,而是因为所有的痛苦都开出了爱的花朵。
在地球上,在你们新的家中,在花园里,在彼此的怀抱中,你们找到了时间的真正含义:不是循环,不是逃避,而是与所爱的人一起度过的每一个宝贵时刻。
这就是你们的故事。这就是爱的胜利。这就是完美的结局。
"""
@end

View File

@@ -10,8 +10,8 @@
// ===== 调查分支故事线 ===== // ===== 调查分支故事线 =====
@node stealth_observation @node stealth_observation_detailed
@title "隐秘观察" @title "隐秘观察 - 详细版"
@audio_bg heartbeat.mp3 @audio_bg heartbeat.mp3
@content """ @content """
你决定保持隐藏,小心翼翼地观察即将到来的访客。 你决定保持隐藏,小心翼翼地观察即将到来的访客。
@@ -131,13 +131,13 @@
""" """
@choices 3 @choices 3
choice_1: "立即现身帮助萨拉" -> crew_confrontation [effect: trust+4] [audio: electronic_tension.mp3] choice_1: "立即现身帮助萨拉" -> crew_confrontation_control_room [effect: trust+4] [audio: electronic_tension.mp3]
choice_2: "继续隐藏,观察即将到来的冲突" -> system_sabotage [effect: secret_unlock] [audio: heartbeat.mp3] choice_2: "继续隐藏,观察即将到来的冲突" -> system_sabotage [effect: secret_unlock] [audio: heartbeat.mp3]
choice_3: "尝试创造分散注意力的行动" -> deception_play [effect: trust+2] [audio: error_alert.mp3] choice_3: "尝试创造分散注意力的行动" -> deception_play [effect: trust+2] [audio: error_alert.mp3]
@end @end
@node data_extraction @node data_extraction
@title "数据提取" @title "访问机密数据库"
@audio_bg discovery_chime.mp3 @audio_bg discovery_chime.mp3
@content """ @content """
利用伊娃的帮助,你开始从基地的数据库中提取关键信息。 利用伊娃的帮助,你开始从基地的数据库中提取关键信息。
@@ -149,7 +149,16 @@
你看到的内容让你震惊: 你看到的内容让你震惊:
时间锚项目的真实目的并不只是防范未来的灾难。它还是一个大规模的意识研究项目,旨在开发人类意识转移技术。 时间锚项目的真实目的并不只是防范未来的灾难。它还是一个大规模的意识研究项目,旨在开发人类意识转移技术。
"""
@choices 1
choice_1: "继续查看地球的真实情况..." -> data_extraction_part2 [audio: button_click.mp3]
@end
@node data_extraction_part2
@title "地球灾难的真相"
@audio_bg discovery_chime.mp3
@content """
地球上的情况比你想象的更糟。气候崩溃已经开始,大部分的生态系统正在死亡。时间锚项目是人类最后的希望——如果不能改变过去,就保存人类的意识。 地球上的情况比你想象的更糟。气候崩溃已经开始,大部分的生态系统正在死亡。时间锚项目是人类最后的希望——如果不能改变过去,就保存人类的意识。
你发现了数百个其他测试对象的记录。大多数都失败了。有些人的意识在转移过程中消散,有些人陷入了永久的昏迷状态。 你发现了数百个其他测试对象的记录。大多数都失败了。有些人的意识在转移过程中消散,有些人陷入了永久的昏迷状态。
@@ -159,7 +168,16 @@
"那其他人呢?其他失败的测试者?" "那其他人呢?其他失败的测试者?"
"他们...他们大多数都死了,艾利克丝。你是唯一一个能够承受这么多循环的人。" "他们...他们大多数都死了,艾利克丝。你是唯一一个能够承受这么多循环的人。"
"""
@choices 1
choice_1: "查看自己的进化数据..." -> data_extraction_final [audio: button_click.mp3]
@end
@node data_extraction_final
@title "意识进化的发现"
@audio_bg discovery_chime.mp3
@content """
数据显示,你的大脑在经历多次循环后发生了某种适应性变化。你的神经网络变得更加有弹性,能够处理时间锚产生的量子干扰。 数据显示,你的大脑在经历多次循环后发生了某种适应性变化。你的神经网络变得更加有弹性,能够处理时间锚产生的量子干扰。
"这意味着什么?" "这意味着什么?"
@@ -167,6 +185,8 @@
"这意味着你不只是一个测试对象,艾利克丝。你已经进化了。你现在拥有独特的能力,可能是人类意识进化的下一个阶段。" "这意味着你不只是一个测试对象,艾利克丝。你已经进化了。你现在拥有独特的能力,可能是人类意识进化的下一个阶段。"
但随着这些信息,你也发现了一个令人恐惧的真相:德米特里计划在完成当前实验后,将你的意识复制到数百个备份中,创造一个"艾利克丝军队"来作为时间锚网络的核心。 但随着这些信息,你也发现了一个令人恐惧的真相:德米特里计划在完成当前实验后,将你的意识复制到数百个备份中,创造一个"艾利克丝军队"来作为时间锚网络的核心。
这个发现让你的血液凝固。你不仅仅是一个实验对象,更是一个即将被大规模复制的模板。
""" """
@choices 4 @choices 4
@@ -213,12 +233,12 @@
@choices 3 @choices 3
choice_1: "让伊娃创造假象,保护她的秘密" -> eva_consultation [effect: trust+10, health-15] [require: trust_level >= 5] [audio: orchestral_revelation.mp3] choice_1: "让伊娃创造假象,保护她的秘密" -> eva_consultation [effect: trust+10, health-15] [require: trust_level >= 5] [audio: orchestral_revelation.mp3]
choice_2: "立即隐藏,放弃当前的破坏行动" -> stealth_observation [effect: health+5] [audio: heartbeat.mp3] choice_2: "立即隐藏,放弃当前的破坏行动" -> stealth_observation_detailed [effect: health+5] [audio: heartbeat.mp3]
choice_3: "继续破坏,准备直接面对德米特里" -> crew_confrontation [effect: trust+3, health-10] [audio: electronic_tension.mp3] choice_3: "继续破坏,准备直接面对德米特里" -> crew_confrontation_control_room [effect: trust+3, health-10] [audio: electronic_tension.mp3]
@end @end
@node crew_confrontation @node crew_confrontation_control_room
@title "团队对峙" @title "控制室对峙"
@audio_bg electronic_tension.mp3 @audio_bg electronic_tension.mp3
@content """ @content """
德米特里博士走进控制室,立即注意到了异常的系统状态。 德米特里博士走进控制室,立即注意到了异常的系统状态。
@@ -269,3 +289,49 @@
choice_2: "尝试说服德米特里投降" -> ethical_discussion [effect: trust+5] [audio: space_silence.mp3] choice_2: "尝试说服德米特里投降" -> ethical_discussion [effect: trust+5] [audio: space_silence.mp3]
choice_3: "要求所有人冷静,寻求和平解决" -> anchor_modification [effect: trust+3, health+10] [audio: orchestral_revelation.mp3] choice_3: "要求所有人冷静,寻求和平解决" -> anchor_modification [effect: trust+3, health+10] [audio: orchestral_revelation.mp3]
@end @end
// ===== 补充的调查分析节点 =====
@node crew_analysis
@title "人员分析与评估"
@audio_bg discovery_chime.mp3
@content """
在这个关键时刻,你需要仔细分析每个基地成员的动机、能力和可信度。
你开始系统性地回顾你对每个人的了解:
**德米特里博士**
- 动机:拯救人类,但方法极端
- 能力:高度的科学知识,对时间锚技术的深入理解
- 可信度:中等,他确实相信自己在做正确的事情,但已经失去道德底线
- 风险:如果感到威胁,可能会采取极端措施
**萨拉博士**
- 动机:保护你和伊娃,对参与实验感到内疚
- 能力:医学专业知识,对记忆重置技术的理解
- 可信度:高,她一直在尝试减少对你的伤害
- 风险:可能会因为恐惧而犹豫不决
**马库斯**
- 动机:保护基地人员,维护正义
- 能力:安全协议,物理保护,武器使用
- 可信度:非常高,在所有循环中都表现出一致的品格
- 风险:缺乏科学背景,可能被误导
**伊娃/莉莉**
- 动机:保护你,寻求自由
- 能力:控制基地系统,数据处理,监控能力
- 可信度:绝对,她是你的妹妹
- 风险作为AI她的行动可能被德米特里切断
基于这个分析,你开始制定策略。你需要确定谁可以成为盟友,谁可能成为威胁,以及如何最好地利用每个人的能力。
"我需要更多信息,"你自语道,"在做出任何重大决定之前。"
"""
@choices 4
choice_1: "深入调查德米特里的真实意图" -> system_sabotage [effect: secret_unlock] [audio: electronic_tension.mp3]
choice_2: "测试萨拉的忠诚度和决心" -> direct_confrontation [effect: trust+2] [audio: heartbeat.mp3]
choice_3: "与马库斯私下讨论安全措施" -> eavesdropping [effect: trust+3] [audio: ambient_mystery.mp3]
choice_4: "请求伊娃提供更详细的基地分析" -> data_extraction [effect: secret_unlock] [audio: orchestral_revelation.mp3]
@end

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
// ===== 支线故事 ===== // ===== 支线故事 =====
@node garden_cooperation @node garden_cooperation
@title "花园中的合作" @title "秘密花园的发现"
@audio_bg wind_gentle.mp3 @audio_bg wind_gentle.mp3
@content """ @content """
萨拉博士带你来到了基地的生物园区——一个你之前从未见过的地方。 萨拉博士带你来到了基地的生物园区——一个你之前从未见过的地方。
@@ -21,7 +21,16 @@
"这是我的秘密花园,"萨拉轻声说道,"当实验和记忆重置让我感到绝望时,我就会来这里。" "这是我的秘密花园,"萨拉轻声说道,"当实验和记忆重置让我感到绝望时,我就会来这里。"
"这里很美,"你真诚地说道,"但为什么要带我来这里?" "这里很美,"你真诚地说道,"但为什么要带我来这里?"
"""
@choices 1
choice_1: "听萨拉的解释..." -> garden_cooperation_part2 [audio: button_click.mp3]
@end
@node garden_cooperation_part2
@title "安全谈话的地点"
@audio_bg wind_gentle.mp3
@content """
萨拉走向一排番茄植株,开始轻柔地检查它们:"因为这里是基地中唯一没有被监控的地方。德米特里认为生物园区只是维持生命支持系统的功能区域,他从不关注这里。" 萨拉走向一排番茄植株,开始轻柔地检查它们:"因为这里是基地中唯一没有被监控的地方。德米特里认为生物园区只是维持生命支持系统的功能区域,他从不关注这里。"
她转身面对你:"这意味着我们可以在这里安全地交谈。" 她转身面对你:"这意味着我们可以在这里安全地交谈。"
@@ -35,7 +44,16 @@
"这些是从地球带来的最后样本,"她解释道,"如果地球真的死亡了,这些可能是这些物种最后的希望。" "这些是从地球带来的最后样本,"她解释道,"如果地球真的死亡了,这些可能是这些物种最后的希望。"
看着这些珍贵的植物,你感受到了一种深深的责任感。 看着这些珍贵的植物,你感受到了一种深深的责任感。
"""
@choices 1
choice_1: "询问萨拉的计划..." -> garden_cooperation_final [audio: button_click.mp3]
@end
@node garden_cooperation_final
@title "记忆恢复的秘密计划"
@audio_bg wind_gentle.mp3
@content """
"萨拉,告诉我你的计划。" "萨拉,告诉我你的计划。"
"我一直在研究记忆重置技术的逆向工程。我相信我们可以创造一个记忆恢复程序,不仅能恢复你被压制的记忆,还能帮助伊娃稳定她的意识。" "我一直在研究记忆重置技术的逆向工程。我相信我们可以创造一个记忆恢复程序,不仅能恢复你被压制的记忆,还能帮助伊娃稳定她的意识。"
@@ -51,11 +69,84 @@
在花园的安静中,你考虑着这个艰难的选择。周围的植物静静地生长着,代表着生命的希望和韧性。 在花园的安静中,你考虑着这个艰难的选择。周围的植物静静地生长着,代表着生命的希望和韧性。
""" """
@choices 4 @choices 6
choice_1: "同意测试记忆恢复程序" -> memory_reconstruction [effect: trust+5, health-20] [require: trust_level >= 4] [audio: time_distortion.mp3] choice_1: "同意测试记忆恢复程序" -> memory_reconstruction [effect: trust+5, health-20] [require: trust_level >= 4] [audio: time_distortion.mp3]
choice_2: "要求更多时间考虑" -> philosophical_discussion [effect: health+10] [audio: space_silence.mp3] choice_2: "要求更多时间考虑" -> philosophical_discussion [effect: health+10] [audio: space_silence.mp3]
choice_3: "提议寻找其他解决方案" -> garden_partnership [effect: trust+3] [audio: wind_gentle.mp3] choice_3: "提议寻找其他解决方案" -> garden_partnership [effect: trust+3] [audio: wind_gentle.mp3]
choice_4: "询问关于地球植物的更多信息" -> earth_truth [effect: secret_unlock] [audio: discovery_chime.mp3] choice_4: "询问关于地球植物的更多信息" -> earth_truth [effect: secret_unlock] [audio: discovery_chime.mp3]
choice_5: "仔细观察花园中的植物细节" -> garden_observation [effect: health+5] [audio: ambient_mystery.mp3]
choice_6: "询问萨拉是否保留了过去的回忆" -> eva_photo_reaction [effect: trust+6] [audio: heartbeat.mp3]
@end
@node garden_observation
@title "花园的生命力"
@audio_bg ambient_mystery.mp3
@content """
你决定花时间仔细观察这个神奇的花园,每一个细节都让你感到惊叹。
在人工光照下,你看到了一些令人难以置信的景象:
**左侧区域**:一排整齐的番茄植株,果实饱满而红润。但更令你惊讶的是,它们的生长密度远超地球标准。萨拉显然掌握了某种高效的栽培技术。
**中央区域**:各种绿叶蔬菜形成了一个小型生态系统。你注意到一些昆虫在其间飞舞——这在月球基地中是不可能的,除非萨拉专门维护了一个完整的生物链。
**右侧区域**:最珍贵的部分。这里种植着一些你从未见过的植物,它们的叶片呈现出奇特的蓝绿色,似乎在微弱地发光。
"那些是什么?"你指着发光的植物问道。
萨拉的眼中闪过一丝骄傲:"那是我的实验项目。通过基因改良,我让它们能够在低光照环境下进行更高效的光合作用。它们不仅能生产氧气,还能产生某些...特殊的化合物。"
"什么样的化合物?"
"神经营养因子。可以帮助大脑修复和增强记忆恢复。这就是我的天然记忆恢复方案的基础。"
你感到震惊。萨拉不仅仅是在种植食物,她在创造药物。
"这需要多长时间才能开发出来的?"
"三年。从第一次循环后,我就开始了这个项目。我知道总有一天,我们会需要一种方法来真正治愈被实验伤害的人们。"
"""
@choices 3
choice_1: "询问植物治疗的具体原理" -> garden_partnership [effect: trust+2] [audio: discovery_chime.mp3]
choice_2: "表达对萨拉的感谢和敬意" -> comfort_session [effect: trust+4] [audio: wind_gentle.mp3]
choice_3: "要求萨拉教你一些园艺技能" -> garden_learning [effect: health+10] [audio: ambient_mystery.mp3]
@end
@node garden_learning
@title "学习生命的艺术"
@audio_bg wind_gentle.mp3
@content """
"能教我一些园艺吗?"你请求道,"我想学习如何照顾这些生命。"
萨拉的脸上露出了真正的笑容,这是你很久没有见过的:"当然。这里的每一株植物都有自己的故事。"
在接下来的时间里,萨拉耐心地教你:
**土壤调配**:如何在月球环境中创造适合植物生长的土壤混合物。她展示了各种矿物质和有机物的精确配比。
**水培技术**:如何通过精确控制营养液来最大化植物生长。她的系统可以根据植物的成长阶段自动调整营养配方。
**光照管理**如何使用不同光谱的LED来模拟地球的自然光照循环甚至可以加速某些生长过程。
**生物循环**:如何维护一个微型生态系统,让植物、微生物和小型昆虫形成完美的平衡。
当你亲手照料一株小番茄苗时,你感受到了一种前所未有的平静。
"这就像...冥想,"你说道。
"是的,"萨拉同意道,"照顾生命让我记住什么是真正重要的。不是实验,不是数据,而是成长、关爱和希望。"
"萨拉,这个花园给了我一个想法。"
"什么想法?"
"如果我们能够在这里创造生命,创造治愈,那么我们也能创造一个更好的未来。不仅仅是通过时间锚技术,而是通过更基本的东西——爱和关怀。"
"""
@choices 2
choice_1: "提议将花园理念扩展到整个基地" -> garden_partnership [effect: trust+5] [audio: orchestral_revelation.mp3]
choice_2: "与萨拉一起制定新的治疗计划" -> memory_reconstruction [effect: trust+3, health+5] [audio: discovery_chime.mp3]
@end @end
@node philosophical_discussion @node philosophical_discussion

View File

@@ -1,7 +1,7 @@
package com.example.gameofmoon package com.osglab.gameofmoon
import android.content.Context import android.content.Context
import com.example.gameofmoon.story.engine.* import com.osglab.gameofmoon.story.engine.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
/** /**

View File

@@ -1,6 +1,7 @@
package com.example.gameofmoon package com.osglab.gameofmoon
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -10,8 +11,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.example.gameofmoon.ui.theme.GameofMoonTheme import androidx.core.view.WindowCompat
import com.example.gameofmoon.presentation.ui.screens.TimeCageGameScreen import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.osglab.gameofmoon.ui.theme.GameofMoonTheme
import com.osglab.gameofmoon.presentation.ui.screens.TimeCageGameScreen
/** /**
* 主活动 * 主活动
@@ -21,6 +25,16 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
// 隐藏状态栏,创建沉浸式全屏体验
WindowCompat.setDecorFitsSystemWindows(window, false)
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// 保持屏幕常亮,适合游戏体验
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContent { setContent {
GameofMoonTheme { GameofMoonTheme {
Surface( Surface(

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.audio package com.osglab.gameofmoon.audio
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.audio package com.osglab.gameofmoon.audio
import android.content.Context import android.content.Context
import android.media.MediaPlayer import android.media.MediaPlayer
@@ -95,12 +95,12 @@ class GameAudioManager(
*/ */
private fun preloadSoundEffects() { private fun preloadSoundEffects() {
val soundEffects = listOf( val soundEffects = listOf(
"button_click" to com.example.gameofmoon.R.raw.button_click, "button_click" to com.osglab.gameofmoon.R.raw.button_click,
"notification_beep" to com.example.gameofmoon.R.raw.notification_beep, "notification_beep" to com.osglab.gameofmoon.R.raw.notification_beep,
"discovery_chime" to com.example.gameofmoon.R.raw.discovery_chime, "discovery_chime" to com.osglab.gameofmoon.R.raw.discovery_chime,
"error_alert" to com.example.gameofmoon.R.raw.error_alert, "error_alert" to com.osglab.gameofmoon.R.raw.error_alert,
"time_distortion" to com.example.gameofmoon.R.raw.time_distortion, "time_distortion" to com.osglab.gameofmoon.R.raw.time_distortion,
"oxygen_leak_alert" to com.example.gameofmoon.R.raw.oxygen_leak_alert "oxygen_leak_alert" to com.osglab.gameofmoon.R.raw.oxygen_leak_alert
) )
soundEffects.forEach { (name, resourceId) -> soundEffects.forEach { (name, resourceId) ->

View File

@@ -1,8 +1,8 @@
package com.example.gameofmoon.data package com.osglab.gameofmoon.data
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import com.example.gameofmoon.model.GameState import com.osglab.gameofmoon.model.GameState
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.data package com.osglab.gameofmoon.data
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.model package com.osglab.gameofmoon.model
/** /**
* 简化的游戏数据模型 * 简化的游戏数据模型

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.presentation.ui.components package com.osglab.gameofmoon.presentation.ui.components
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
@@ -6,35 +6,31 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.example.gameofmoon.ui.theme.GameofMoonTheme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.example.gameofmoon.model.GameState import com.osglab.gameofmoon.model.GameState
import com.example.gameofmoon.model.CharacterStatus import com.osglab.gameofmoon.model.CharacterStatus
// 基本赛博朋克色彩定义 // 基本赛博朋克色彩定义
private val CyberBlue = Color(0xFF00FFFF) private val CyberBlue = Color(0xFF00FFFF)
private val CyberGreen = Color(0xFF39FF14) private val CyberGreen = Color(0xFF39FF14)
private val DarkBackground = Color(0xFF0A0A0A) private val DarkBackground = Color.Transparent // 完全透明,让背景完全透出
private val DarkSurface = Color(0xFF151515) private val DarkSurface = Color.Transparent // 完全透明,让背景完全透出
private val DarkCard = Color(0xFF1E1E1E) private val DarkCard = Color(0x20000000) // 极淡透明,仅保持基本可读性
private val DarkBorder = Color(0xFF333333) private val DarkBorder = Color(0xFF333333)
private val TextPrimary = Color(0xFFE0E0E0) private val TextPrimary = Color(0xFFE0E0E0)
private val TextSecondary = Color(0xFFB0B0B0) private val TextSecondary = Color(0xFFB0B0B0)
@@ -43,7 +39,7 @@ private val TextAccent = Color(0xFF00FFFF)
private val ErrorRed = Color(0xFFFF0040) private val ErrorRed = Color(0xFFFF0040)
private val WarningOrange = Color(0xFFFF8800) private val WarningOrange = Color(0xFFFF8800)
private val SuccessGreen = Color(0xFF00FF88) private val SuccessGreen = Color(0xFF00FF88)
private val ScanlineColor = Color(0x1100FFFF) // 移除了ScanlineColor常量
// 字体样式定义 // 字体样式定义
object CyberTextStyles { object CyberTextStyles {
@@ -82,8 +78,8 @@ object CyberTextStyles {
val CompactChoice = androidx.compose.ui.text.TextStyle( val CompactChoice = androidx.compose.ui.text.TextStyle(
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium, fontWeight = androidx.compose.ui.text.font.FontWeight.Medium,
fontSize = 11.sp, // 较小字体 fontSize = 12.sp, // 增大字体一号,保持可读性
lineHeight = 16.sp, // 较小行高 lineHeight = 8.sp, // 保持紧凑的行高控制按钮高度
letterSpacing = 0.2.sp letterSpacing = 0.2.sp
) )
@@ -106,25 +102,19 @@ fun TerminalWindow(
isActive: Boolean = true, isActive: Boolean = true,
content: @Composable BoxScope.() -> Unit content: @Composable BoxScope.() -> Unit
) { ) {
val borderColor by animateColorAsState( // 简化为静态颜色,移除动画
targetValue = if (isActive) CyberBlue else DarkBorder, val borderColor = if (isActive) CyberBlue else DarkBorder
animationSpec = tween(300),
label = "border_color"
)
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.background(DarkBackground) .border(1.dp, borderColor) // 只保留边框,移除背景
.border(1.dp, borderColor)
.background(DarkSurface.copy(alpha = 0.9f))
) { ) {
// 标题栏 // 标题栏 - 完全透明
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(DarkCard) .padding(horizontal = 12.dp, vertical = 8.dp), // 移除背景,只保留内边距
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -163,43 +153,11 @@ fun TerminalWindow(
content() content()
} }
// 扫描线效果 // 移除扫描线效果以简化界面
if (isActive) {
ScanlineEffect()
}
} }
} }
/** // 移除了ScanlineEffect组件以简化界面
* 扫描线效果组件
*/
@Composable
private fun BoxScope.ScanlineEffect() {
val infiniteTransition = rememberInfiniteTransition(label = "scanline")
val scanlinePosition by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "scanline_position"
)
Box(
modifier = Modifier
.fillMaxSize()
.drawBehind {
val scanlineY = size.height * scanlinePosition
drawLine(
color = ScanlineColor,
start = Offset(0f, scanlineY),
end = Offset(size.width, scanlineY),
strokeWidth = 2.dp.toPx()
)
}
)
}
/** /**
* 华丽的霓虹发光按钮 * 华丽的霓虹发光按钮
@@ -220,39 +178,10 @@ fun NeonButton(
compact: Boolean = false, // 紧凑模式,减少内边距 compact: Boolean = false, // 紧凑模式,减少内边距
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit
) { ) {
val infiniteTransition = rememberInfiniteTransition(label = "neon_button_animation") // 简化为静态值,移除动画以提升性能
val animatedGlow = if (enabled) 0.8f else 0.3f
// 基础发光强度 val pulseGlow = 0.8f // 固定发光强度
val animatedGlow by animateFloatAsState( val hoverScale = 1f // 移除缩放动画
targetValue = if (enabled) 1f else 0.3f,
animationSpec = tween(300),
label = "glow_animation"
)
// 脉冲效果
val pulseGlow by infiniteTransition.animateFloat(
initialValue = 0.6f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 2000,
easing = EaseInOutSine
),
repeatMode = RepeatMode.Reverse
),
label = "pulse_glow"
)
// 悬停状态检测
var isHovered by remember { mutableStateOf(false) }
val hoverScale by animateFloatAsState(
targetValue = if (isHovered && enabled) 1.05f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "hover_scale"
)
Button( Button(
onClick = onClick, onClick = onClick,
@@ -329,7 +258,7 @@ fun NeonButton(
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
if (enabled) { if (enabled) {
isHovered = !isHovered // 静态版本不处理hover状态
} }
}, },
enabled = enabled, enabled = enabled,
@@ -341,9 +270,9 @@ fun NeonButton(
pressedElevation = if (enabled) 8.dp else 0.dp pressedElevation = if (enabled) 8.dp else 0.dp
), ),
contentPadding = if (compact) { contentPadding = if (compact) {
PaddingValues(horizontal = 12.dp, vertical = 6.dp) // 调整紧凑模式内边距 PaddingValues(horizontal = 12.dp, vertical = 2.dp) // 最小化紧凑模式按钮高度
} else { } else {
PaddingValues(horizontal = 16.dp, vertical = 8.dp) // 调整默认内边距 PaddingValues(horizontal = 16.dp, vertical = 4.dp) // 减少默认按钮高度
}, },
content = content content = content
) )
@@ -436,11 +365,8 @@ fun CyberProgressBar(
showPercentage: Boolean = true, showPercentage: Boolean = true,
animated: Boolean = true animated: Boolean = true
) { ) {
val animatedProgress by animateFloatAsState( // 简化为静态值,移除动画
targetValue = if (animated) progress else progress, val animatedProgress = progress
animationSpec = tween(500),
label = "progress_animation"
)
Column(modifier = modifier) { Column(modifier = modifier) {
if (showPercentage) { if (showPercentage) {
@@ -564,14 +490,8 @@ fun StatusIndicator(
StatusType.PROCESSING -> CyberBlue to "" StatusType.PROCESSING -> CyberBlue to ""
} }
val animatedAlpha by animateFloatAsState( // 简化状态显示,移除闪烁动画
targetValue = if (status == StatusType.PROCESSING) 0.5f else 1f, val animatedAlpha = if (status == StatusType.PROCESSING) 0.7f else 1f
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "status_blink"
)
Row( Row(
modifier = modifier, modifier = modifier,
@@ -608,32 +528,14 @@ fun CyberDivider(
thickness: Float = 1f, thickness: Float = 1f,
animated: Boolean = false animated: Boolean = false
) { ) {
if (animated) { // 简化为静态分割线,移除动画
val infiniteTransition = rememberInfiniteTransition(label = "divider_animation") val alpha = if (animated) 0.6f else 1f
val animatedAlpha by infiniteTransition.animateFloat( Box(
initialValue = 0.3f, modifier = modifier
targetValue = 1f, .fillMaxWidth()
animationSpec = infiniteRepeatable( .height(thickness.dp)
animation = tween(2000), .background(color.copy(alpha = alpha))
repeatMode = RepeatMode.Reverse )
),
label = "divider_alpha"
)
Box(
modifier = modifier
.fillMaxWidth()
.height(thickness.dp)
.background(color.copy(alpha = animatedAlpha))
)
} else {
Box(
modifier = modifier
.fillMaxWidth()
.height(thickness.dp)
.background(color)
)
}
} }
/** /**
@@ -647,7 +549,7 @@ fun CompactStatusBar(
) { ) {
Surface( Surface(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
color = Color(0xFF0A0A0A), color = Color.Transparent, // 完全透明,让背景完全透出
shadowElevation = 4.dp shadowElevation = 4.dp
) { ) {
Row( Row(
@@ -716,151 +618,36 @@ fun CompactStatusBar(
} }
/** /**
* 华丽的故事内容窗口组件 * 简洁的故事内容窗口组件
* 带有发光边框、玻璃质感背景和动态效果 * 专门为故事内容显示设计,无装饰边框
* 专门为故事内容和选择按钮设计
*/ */
@Composable @Composable
fun StoryContentWindow( fun StoryContentWindow(
title: String, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isActive: Boolean = true,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
val infiniteTransition = rememberInfiniteTransition(label = "story_window_animation")
// 发光边框颜色动画
val glowIntensity by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 2000,
easing = EaseInOutSine
),
repeatMode = RepeatMode.Reverse
),
label = "glow_intensity"
)
val borderColor by animateColorAsState(
targetValue = if (isActive) CyberBlue.copy(alpha = glowIntensity) else DarkBorder,
animationSpec = tween(300),
label = "border_color"
)
// 背景渐变动画
val backgroundAlpha by infiniteTransition.animateFloat(
initialValue = 0.15f,
targetValue = 0.25f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 4000,
easing = EaseInOutCubic
),
repeatMode = RepeatMode.Reverse
),
label = "background_alpha"
)
Box( Box(
modifier = modifier.fillMaxWidth() modifier = modifier.fillMaxWidth()
) { ) {
// 华丽的背景层 // 简洁的内容容器 - 无边框无阴影
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.background(
// 玻璃质感的渐变背景
brush = Brush.verticalGradient(
colors = listOf(
Color(0x40001133), // 顶部深蓝透明
Color(0x20000814), // 中部深灰透明
Color(0x30001122) // 底部深蓝透明
)
)
)
.border(
width = 2.dp,
brush = Brush.linearGradient(
colors = listOf(
borderColor,
CyberBlue.copy(alpha = glowIntensity * 0.6f),
borderColor
)
),
shape = RectangleShape
)
.shadow(
elevation = (8 * glowIntensity).dp,
spotColor = CyberBlue,
ambientColor = CyberBlue.copy(alpha = 0.3f)
)
) { ) {
// 华丽的标题栏 // 简洁的标题栏
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background( .padding(horizontal = 12.dp, vertical = 8.dp), // 减少标题栏边距
brush = Brush.horizontalGradient( horizontalArrangement = Arrangement.Start, // 标题左对齐,移除右侧按钮
colors = listOf(
Color(0x60001122),
Color(0x80002244),
Color(0x60001122)
)
)
)
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = title, text = title,
style = CyberTextStyles.Terminal.copy( style = CyberTextStyles.Terminal, // 移除发光阴影效果
shadow = Shadow( color = Color(0xFFAADDFF) // 简化颜色,去除复杂逻辑
color = if (isActive) CyberBlue.copy(alpha = 0.8f) else Color.Transparent,
blurRadius = 6f
)
),
color = if (isActive) Color(0xFFAADDFF) else TextSecondary
) )
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
// 发光的终端控制按钮
repeat(3) { index ->
val color = when (index) {
0 -> ErrorRed
1 -> WarningOrange
else -> SuccessGreen
}
val buttonGlow by infiniteTransition.animateFloat(
initialValue = 0.5f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1500 + index * 200, // 错开动画时间
easing = EaseInOutSine
),
repeatMode = RepeatMode.Reverse
),
label = "button_glow_$index"
)
Box(
modifier = Modifier
.size(8.dp)
.background(color.copy(alpha = buttonGlow), CircleShape)
.shadow(
elevation = (2 * buttonGlow).dp,
shape = CircleShape,
spotColor = color
)
)
}
}
} }
// 华丽的内容区域 // 华丽的内容区域
@@ -868,57 +655,14 @@ fun StoryContentWindow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) // 自动填充剩余空间 .weight(1f) // 自动填充剩余空间
.padding(12.dp) .padding(12.dp) // 减少内容区域边距,保持适度留白
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.background( // 移除背景渐变,让背景完全透出
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color(0x10001122).copy(alpha = backgroundAlpha),
Color.Transparent
)
)
)
) { ) {
content() content()
} }
// 增强的扫描线效果覆盖层 // 移除了扫描线效果以简化界面
if (isActive) {
val scanlineOffset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = "scanline_offset"
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(3.dp)
.offset(y = (scanlineOffset * 200).dp) // 动态移动扫描线
.background(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
CyberBlue.copy(alpha = 0.8f),
Color(0xFF00DDFF).copy(alpha = 1f),
CyberBlue.copy(alpha = 0.8f),
Color.Transparent
)
)
)
.shadow(
elevation = 4.dp,
spotColor = CyberBlue
)
)
} }
} }
} }

View File

@@ -1,233 +1,162 @@
package com.example.gameofmoon.presentation.ui.components package com.osglab.gameofmoon.presentation.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import com.example.gameofmoon.presentation.ui.theme.GameIcons import com.osglab.gameofmoon.R
import com.osglab.gameofmoon.presentation.ui.theme.GameIcons
@Composable @Composable
fun GameControlMenu( fun GameControlMenu(
isVisible: Boolean, isVisible: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSaveGame: () -> Unit, onNewLoop: () -> Unit
onLoadGame: () -> Unit,
onNewLoop: () -> Unit,
onAiAssist: () -> Unit,
onShowHistory: () -> Unit,
onSettings: () -> Unit
) { ) {
if (isVisible) { if (isVisible) {
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
TerminalWindow( Box(
title = "游戏控制中心",
modifier = Modifier modifier = Modifier
.width(320.dp) .fillMaxSize()
.heightIn(max = 520.dp) .padding(horizontal = 5.dp)
.background(Color.Black.copy(alpha = 0.5f)), // 70%透明度的黑色背景
contentAlignment = Alignment.Center
) { ) {
Column( Box(
verticalArrangement = Arrangement.spacedBy(8.dp) modifier = Modifier
.width(380.dp)
.height(570.dp)
) { ) {
// 保存/读取组 // 设置背景图片
Text( Image(
text = "数据管理", painter = painterResource(id = R.drawable.setting),
style = CyberTextStyles.Choice.copy(fontSize = 14.sp), contentDescription = "Setting Background",
color = Color(0xFF00DDFF), modifier = Modifier.fillMaxSize(),
fontWeight = FontWeight.Bold contentScale = ContentScale.FillBounds
) )
Row( // 内容覆盖在背景图片上
modifier = Modifier.fillMaxWidth(), Column(
horizontalArrangement = Arrangement.spacedBy(8.dp) modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp, vertical = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 顶部额外留白
Spacer(modifier = Modifier.height(32.dp))
// 游戏Logo区域
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 游戏Logo图片
Image(
painter = painterResource(id = R.drawable.game_logo),
contentDescription = "Game of Moon Logo",
modifier = Modifier
.size(100.dp)
.padding(4.dp),
contentScale = ContentScale.Fit
)
// 副标题
Text(
text = "时间囚笼",
style = CyberTextStyles.Choice.copy(fontSize = 14.sp),
color = Color(0xFF00AACC),
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(20.dp))
// 游戏介绍文本
Text(
text = "在无尽的时间循环中探索真相,\n每一次重启都是新的开始,\n但记忆将指引你走向不同的结局。",
style = CyberTextStyles.StoryContent.copy(fontSize = 12.sp),
color = Color(0xFFCCCCCC),
textAlign = TextAlign.Center,
lineHeight = 16.sp
)
// 将按钮推到底部
Spacer(modifier = Modifier.weight(1f))
// 开始新循环按钮
NeonButton( NeonButton(
onClick = { onClick = {
onSaveGame() onNewLoop()
onDismiss() onDismiss()
}, },
modifier = Modifier.weight(1f) modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp, end = 15.dp, bottom = 2.dp)
) { ) {
Column( Row(
horizontalAlignment = Alignment.CenterHorizontally modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = GameIcons.Save, imageVector = GameIcons.Refresh,
contentDescription = "保存", contentDescription = "开始新循环",
modifier = Modifier.size(24.dp), modifier = Modifier.size(20.dp),
tint = Color(0xFF00DDFF) tint = Color(0xFF00DDFF)
) )
Text("保存", fontSize = 12.sp) Spacer(modifier = Modifier.width(8.dp))
} Text(
} text = "开始新循环",
fontSize = 14.sp,
NeonButton( fontWeight = FontWeight.Bold
onClick = {
onLoadGame()
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = GameIcons.Load,
contentDescription = "读取",
modifier = Modifier.size(24.dp),
tint = Color(0xFF00DDFF)
) )
Text("读取", fontSize = 12.sp)
}
}
}
CyberDivider()
// 游戏控制组
Text(
text = "游戏控制",
style = CyberTextStyles.Choice.copy(fontSize = 14.sp),
color = Color(0xFF00DDFF),
fontWeight = FontWeight.Bold
)
NeonButton(
onClick = {
onNewLoop()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = GameIcons.Refresh,
contentDescription = "开始新循环",
modifier = Modifier.size(20.dp),
tint = Color(0xFF00DDFF)
)
Column {
Text("开始新循环", fontSize = 12.sp, fontWeight = FontWeight.Bold)
Text("重置进度,保留记忆", fontSize = 10.sp, color = Color(0xFFAAAA88))
}
}
}
NeonButton(
onClick = {
onShowHistory()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = GameIcons.History,
contentDescription = "对话历史",
modifier = Modifier.size(20.dp),
tint = Color(0xFF00DDFF)
)
Column {
Text("对话历史", fontSize = 12.sp, fontWeight = FontWeight.Bold)
Text("查看完整对话记录", fontSize = 10.sp, color = Color(0xFFAAAA88))
}
}
}
CyberDivider()
// AI助手组
Text(
text = "AI助手",
style = CyberTextStyles.Choice.copy(fontSize = 14.sp),
color = Color(0xFF00DDFF),
fontWeight = FontWeight.Bold
)
NeonButton(
onClick = {
onAiAssist()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = GameIcons.Robot,
contentDescription = "AI协助",
modifier = Modifier.size(20.dp),
tint = Color(0xFF00DDFF)
)
Column {
Text("请求AI协助", fontSize = 12.sp, fontWeight = FontWeight.Bold)
Text("生成新的故事内容", fontSize = 10.sp, color = Color(0xFFAAAA88))
}
}
}
CyberDivider()
// 设置组
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
NeonButton(
onClick = {
onSettings()
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = GameIcons.Settings,
contentDescription = "设置",
modifier = Modifier.size(24.dp),
tint = Color(0xFF00DDFF)
)
Text("设置", fontSize = 12.sp)
} }
} }
Spacer(modifier = Modifier.height(8.dp))
// 关闭按钮
NeonButton( NeonButton(
onClick = onDismiss, onClick = onDismiss,
modifier = Modifier.weight(1f) modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp, end = 15.dp, bottom = 5.dp)
) { ) {
Column( Row(
horizontalAlignment = Alignment.CenterHorizontally modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = GameIcons.Close, imageVector = GameIcons.Close,
contentDescription = "关闭", contentDescription = "关闭",
modifier = Modifier.size(24.dp), modifier = Modifier.size(20.dp),
tint = Color(0xFF00DDFF) tint = Color(0xFF00DDFF)
) )
Text("关闭", fontSize = 12.sp) Spacer(modifier = Modifier.width(8.dp))
Text(
text = "关闭",
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
} }
} }
// 底部额外留白
Spacer(modifier = Modifier.height(32.dp))
} }
} } // 背景图片Box结尾
} } // 外层Box结尾
} }
} }
} }

View File

@@ -0,0 +1,191 @@
package com.osglab.gameofmoon.presentation.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.osglab.gameofmoon.R
/**
* 9宫格自适应背景组件
* 使用9个分离的图片文件实现完美的边框自适应效果
*
* 布局结构:
* ┌─────────┬─────────────┬─────────┐
* │top_left │ top_mid │top_right│ <- 顶部行
* ├─────────┼─────────────┼─────────┤
* │mid_left │ mid_mid │mid_right│ <- 中间行
* ├─────────┼─────────────┼─────────┤
* │bot_left │ bot_mid │bot_right│ <- 底部行
* └─────────┴─────────────┴─────────┘
*
* 统一尺寸系统设计(解决对齐问题):
* - 顶部行(固定100dp高): top_left(100×100) + top_mid(weight×100) + top_right(100×100)
* - 中间行(动态填充高度): mid_left(100×fill) + mid_mid(weight×fill) + mid_right(100×fill)
* - 底部行(固定100dp高): bot_left(100×100) + bot_mid(weight×100) + bot_right(100×100)
*
* 🔑 关键设计决策:
* - 统一使用Image组件 + ContentScale.FillBounds 确保精确尺寸控制
* - 统一使用100.dp基准避免密度相关的显示差异
* - 宽度布局100dp + weight(1f) + 100dp = 完美对齐
* - 高度布局100dp + weight(1f) + 100dp = 完美对称
* - 解决原问题消除50%显示、不可见、100%错位等问题
*/
@Composable
fun NinePatchBackground(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit = {}
) {
Box(modifier = modifier.fillMaxSize()) {
// 使用Column和Row来创建9宫格布局
Column(modifier = Modifier.fillMaxSize()) {
// 顶部行统一使用100dp尺寸基准
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp) // 统一的dp基准
) {
// 左上角 - 固定100dp×100dp
Image(
painter = painterResource(id = R.drawable.bgframe_top_left),
contentDescription = "左上角边框",
contentScale = ContentScale.FillBounds, // 拉伸到指定尺寸
modifier = Modifier.size(100.dp, 100.dp)
)
// 顶部中间 - 水平填充高度100dp
Image(
painter = painterResource(id = R.drawable.bgframe_top_mid),
contentDescription = "顶部边框",
contentScale = ContentScale.FillBounds, // 拉伸填充
modifier = Modifier
.weight(1f) // 占据剩余空间
.height(100.dp)
)
// 右上角 - 固定100dp×100dp
Image(
painter = painterResource(id = R.drawable.bgframe_top_right),
contentDescription = "右上角边框",
contentScale = ContentScale.FillBounds, // 拉伸到指定尺寸
modifier = Modifier.size(100.dp, 100.dp)
)
}
// 中间行统一使用100dp宽度基准
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f) // 占据剩余垂直空间
) {
// 左边中间 - 固定100dp宽度
Image(
painter = painterResource(id = R.drawable.bgframe_mid_left),
contentDescription = "左侧边框",
contentScale = ContentScale.FillBounds, // 拉伸填充
modifier = Modifier
.width(100.dp) // 统一宽度
.fillMaxHeight()
)
// 中心区域 - 填充剩余空间
Image(
painter = painterResource(id = R.drawable.bgframe_mid_mid),
contentDescription = "中心背景",
contentScale = ContentScale.FillBounds, // 拉伸填充
modifier = Modifier
.weight(1f)
.fillMaxHeight()
)
// 右边中间 - 固定100dp宽度
Image(
painter = painterResource(id = R.drawable.bgframe_mid_right),
contentDescription = "右侧边框",
contentScale = ContentScale.FillBounds, // 拉伸填充
modifier = Modifier
.width(100.dp) // 统一宽度
.fillMaxHeight()
)
}
// 底部行统一使用100dp尺寸基准
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp) // 统一的dp基准
) {
// 左下角 - 固定100dp×100dp
Image(
painter = painterResource(id = R.drawable.bgframe_bot_left),
contentDescription = "左下角边框",
contentScale = ContentScale.FillBounds, // 拉伸到指定尺寸
modifier = Modifier.size(100.dp, 100.dp)
)
// 底部中间 - 水平填充高度100dp
Image(
painter = painterResource(id = R.drawable.bgframe_bot_mid),
contentDescription = "底部边框",
contentScale = ContentScale.FillBounds, // 拉伸填充
modifier = Modifier
.weight(1f) // 占据剩余空间
.height(100.dp)
)
// 右下角 - 固定100dp×100dp
Image(
painter = painterResource(id = R.drawable.bgframe_bot_right),
contentDescription = "右下角边框",
contentScale = ContentScale.FillBounds, // 拉伸到指定尺寸
modifier = Modifier.size(100.dp, 100.dp)
)
}
}
// 内容层 - 在背景之上
content()
}
}
/**
* 带内边距的9宫格背景
* 确保内容不会被边框遮挡
*/
@Composable
fun NinePatchBackgroundWithPadding(
modifier: Modifier = Modifier,
paddingPercent: Float = 0.08f, // 增加内边距因为9宫格边框可能较宽
content: @Composable BoxScope.() -> Unit = {}
) {
Box(modifier = modifier.fillMaxSize()) {
// 9宫格背景
NinePatchBackground()
// 内容区域 - 带内边距避免被边框遮挡
Box(
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = (paddingPercent * 100).toInt().dp,
vertical = (paddingPercent * 80).toInt().dp // 垂直方向稍微少一些
)
) {
content()
}
}
}
@Preview(showBackground = true)
@Composable
fun NinePatchBackgroundPreview() {
NinePatchBackground {
// 预览内容
}
}

View File

@@ -0,0 +1,49 @@
package com.osglab.gameofmoon.presentation.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* 科幻金属边框背景组件
* 使用9宫格自适应背景完美适配各种屏幕尺寸
*/
@Composable
fun SciFiBackground(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit = {}
) {
// 直接使用9宫格背景组件
NinePatchBackground(
modifier = modifier,
content = content
)
}
/**
* 带内边距的科幻背景
* 确保内容不会被边框遮挡使用9宫格自适应背景
*/
@Composable
fun SciFiBackgroundWithPadding(
modifier: Modifier = Modifier,
paddingPercent: Float = 0.08f, // 增加内边距因为9宫格边框可能较宽
content: @Composable BoxScope.() -> Unit = {}
) {
// 直接使用带内边距的9宫格背景组件
NinePatchBackgroundWithPadding(
modifier = modifier,
paddingPercent = paddingPercent,
content = content
)
}
@Preview(showBackground = true)
@Composable
fun SciFiBackgroundPreview() {
SciFiBackground {
// 使用9宫格自适应背景的预览
}
}

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.presentation.ui.components package com.osglab.gameofmoon.presentation.ui.components
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
@@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.random.Random import kotlin.random.Random
import com.example.gameofmoon.presentation.ui.theme.GameIcons import com.osglab.gameofmoon.presentation.ui.theme.GameIcons
/** /**
* 字符动画状态数据类 * 字符动画状态数据类

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.presentation.ui.screens package com.osglab.gameofmoon.presentation.ui.screens
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -13,19 +13,22 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import com.example.gameofmoon.data.GameSaveManager import com.osglab.gameofmoon.data.GameSaveManager
import com.example.gameofmoon.data.SimpleGeminiService import com.osglab.gameofmoon.data.SimpleGeminiService
import com.example.gameofmoon.model.* import com.osglab.gameofmoon.model.*
import com.example.gameofmoon.story.CompleteStoryData // 旧系统已移除,不再使用 CompleteStoryData
import com.example.gameofmoon.story.engine.StoryEngineAdapter import com.osglab.gameofmoon.story.engine.StoryEngineAdapter
import com.example.gameofmoon.presentation.ui.components.* import com.osglab.gameofmoon.presentation.ui.components.*
import com.example.gameofmoon.presentation.ui.theme.GameIcons import com.osglab.gameofmoon.presentation.ui.theme.GameIcons
import com.example.gameofmoon.audio.* import com.osglab.gameofmoon.audio.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
@Composable @Composable
fun TimeCageGameScreen() { fun TimeCageGameScreen() {
@@ -89,11 +92,11 @@ fun TimeCageGameScreen() {
throw Exception("Engine initialization failed") throw Exception("Engine initialization failed")
} }
} catch (e: Exception) { } catch (e: Exception) {
// 如果新引擎失败fallback到旧系统 // 强制采用新引擎;初始化失败时给出简要提示
currentNode = CompleteStoryData.getStoryNode("first_awakening") ?: SimpleStoryNode( currentNode = SimpleStoryNode(
id = "fallback", id = "engine_init_error",
title = "引擎初始化失败", title = "引擎初始化失败",
content = "正在使用备用故事系统...", content = "故事引擎初始化失败,请重试或查看日志。",
choices = emptyList() choices = emptyList()
) )
} }
@@ -104,6 +107,7 @@ fun TimeCageGameScreen() {
var gameMessage by remember { mutableStateOf("欢迎来到时间囚笼!第${gameState.currentLoop}次循环开始。") } var gameMessage by remember { mutableStateOf("欢迎来到时间囚笼!第${gameState.currentLoop}次循环开始。") }
var showControlMenu by remember { mutableStateOf(false) } var showControlMenu by remember { mutableStateOf(false) }
var showDialogueHistory by remember { mutableStateOf(false) } var showDialogueHistory by remember { mutableStateOf(false) }
val canNavigateBack by storyEngineAdapter.canNavigateBack.collectAsState()
// 检查游戏结束条件 // 检查游戏结束条件
LaunchedEffect(gameState.health) { LaunchedEffect(gameState.health) {
@@ -112,38 +116,25 @@ fun TimeCageGameScreen() {
storyEngineAdapter.navigateToNode("game_over_failure") storyEngineAdapter.navigateToNode("game_over_failure")
gameMessage = "健康值耗尽...循环重置" gameMessage = "健康值耗尽...循环重置"
} catch (e: Exception) { } catch (e: Exception) {
// Fallback到旧系统 gameMessage = "健康值耗尽,且引擎导航失败"
currentNode = CompleteStoryData.getStoryNode("game_over_failure") ?: currentNode
gameMessage = "健康值耗尽...循环重置"
} }
} }
} }
Box( // 使用科幻金属边框背景
modifier = Modifier.fillMaxSize() SciFiBackgroundWithPadding(
modifier = Modifier.fillMaxSize(),
paddingPercent = 0.25f // 增加到12% 内边距,提供更多呼吸空间
) { ) {
// 华丽的背景层
CyberpunkBackground(
modifier = Modifier.fillMaxSize(),
enableStars = true,
enableParticles = true,
enableScanlines = true,
enableMatrixRain = false, // 先关闭数字雨,避免干扰阅读
enableGridLines = false, // 先关闭网格,保持简洁
starCount = 120,
particleCount = 30
)
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxSize() // 移除statusBarsPadding因为要隐藏状态栏
.fillMaxSize()
.statusBarsPadding()
) { ) {
// 顶部固定区域:标题和快捷按钮 // 顶部固定区域:标题和快捷按钮
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp), // 减少垂直间距 .padding(horizontal = 16.dp, vertical = 12.dp), // 增加留白提供更好的视觉空间
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -179,7 +170,7 @@ fun TimeCageGameScreen() {
modifier = Modifier modifier = Modifier
.size(36.dp) // 稍微减小按钮 .size(36.dp) // 稍微减小按钮
.background( .background(
Color(0xFF003366), Color.Transparent, // 改为透明,让背景完全透出
shape = CircleShape shape = CircleShape
) )
) { ) {
@@ -200,7 +191,7 @@ fun TimeCageGameScreen() {
modifier = Modifier modifier = Modifier
.size(36.dp) // 稍微减小按钮 .size(36.dp) // 稍微减小按钮
.background( .background(
Color(0xFF003366), Color.Transparent, // 改为透明,让背景完全透出
shape = CircleShape shape = CircleShape
) )
) { ) {
@@ -219,8 +210,8 @@ fun TimeCageGameScreen() {
title = "📖 ${currentNode.title}", title = "📖 ${currentNode.title}",
modifier = Modifier modifier = Modifier
.weight(1f) // 占用剩余空间 .weight(1f) // 占用剩余空间
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp) // 减少水平padding让内容更紧凑
.padding(bottom = 12.dp) .padding(bottom = 10.dp) // 减少底部padding
) { ) {
// 故事文本 - 使用打字机效果 // 故事文本 - 使用打字机效果
StoryTypewriterText( StoryTypewriterText(
@@ -237,25 +228,25 @@ fun TimeCageGameScreen() {
Text( Text(
text = "DEBUG: 节点=${currentNode.id} | 内容长度=${currentNode.content.length} | 选择=${currentNode.choices.size}", text = "DEBUG: 节点=${currentNode.id} | 内容长度=${currentNode.content.length} | 选择=${currentNode.choices.size}",
style = CyberTextStyles.Caption, style = CyberTextStyles.Caption,
color = Color(0xFF444444), color = Color(0xFF888888), // 调亮一点,增强可读性
modifier = Modifier.padding(top = 8.dp), modifier = Modifier.padding(top = 8.dp),
fontSize = 9.sp fontSize = 9.sp
) )
} }
} }
// 底部固定操作区 - 选择按钮 // 底部固定操作区 - 选择按钮(完全透明背景)
if (currentNode.choices.isNotEmpty()) { if (currentNode.choices.isNotEmpty()) {
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = Color(0xFF0A0A0A), color = Color.Transparent, // 改为透明,让科幻背景完全透出
shadowElevation = 8.dp shadowElevation = 8.dp
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp), // 减少垂直内边距 .padding(horizontal = 24.dp, vertical = 16.dp), // 再增加2%边距,提供更宽敞的操作空间
verticalArrangement = Arrangement.spacedBy(6.dp) // 减少组件间距 verticalArrangement = Arrangement.spacedBy(3.dp) // 进一步减少组件间距,让按钮区域更紧凑
) { ) {
CyberDivider() CyberDivider()
@@ -281,18 +272,13 @@ fun TimeCageGameScreen() {
throw Exception("Choice execution failed") throw Exception("Choice execution failed")
} }
} catch (e: Exception) { } catch (e: Exception) {
// Fallback到旧系统 gameMessage = "选择执行失败:${'$'}{e.message}"
val nextNode = CompleteStoryData.getStoryNode(choice.nextNodeId)
if (nextNode != null) {
currentNode = nextNode
gameMessage = "你选择了:${choice.text} (备用系统)"
}
} }
} }
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 1.dp), // 减少垂直间距 .padding(vertical = 0.5.dp), // 进一步减少按钮间距
compact = true // 使用紧凑模式 compact = true // 使用紧凑模式
) { ) {
Text( Text(
@@ -305,14 +291,37 @@ fun TimeCageGameScreen() {
} }
} }
} // Column 结尾 } // Column 结尾
} // Box 结尾
// 调试用:全局悬浮“返回上一个节点”按钮(始终可见)
Box(
modifier = Modifier.fillMaxSize()
) {
FloatingActionButton(
onClick = {
audioController.playSoundEffect("button_click")
coroutineScope.launch {
storyEngineAdapter.navigateBack()
}
},
containerColor = if (canNavigateBack) Color(0xCC111111) else Color(0x66111111),
contentColor = if (canNavigateBack) Color(0xFFFFD700) else Color(0xFF888888),
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
.size(52.dp)
) {
Icon(
imageVector = GameIcons.History,
contentDescription = "返回上一个节点",
modifier = Modifier.size(24.dp)
)
}
}
// 游戏控制菜单弹窗 // 游戏控制菜单弹窗
GameControlMenu( GameControlMenu(
isVisible = showControlMenu, isVisible = showControlMenu,
onDismiss = { showControlMenu = false }, onDismiss = { showControlMenu = false },
onSaveGame = { /* 暂时简化 */ },
onLoadGame = { /* 暂时简化 */ },
onNewLoop = { onNewLoop = {
// 重新开始游戏 // 重新开始游戏
gameState = GameState(currentLoop = gameState.currentLoop + 1) gameState = GameState(currentLoop = gameState.currentLoop + 1)
@@ -324,47 +333,43 @@ fun TimeCageGameScreen() {
throw Exception("New game start failed") throw Exception("New game start failed")
} }
} catch (e: Exception) { } catch (e: Exception) {
// Fallback到旧系统 gameMessage = "新循环启动失败:${'$'}{e.message}"
currentNode = CompleteStoryData.getStoryNode("first_awakening") ?: currentNode
gameMessage = "${gameState.currentLoop}次循环开始!(备用系统)"
} }
} }
dialogueHistory = emptyList() dialogueHistory = emptyList()
}, }
onAiAssist = { /* 暂时简化 */ },
onShowHistory = { /* 暂时简化 */ },
onSettings = { /* 暂时简化 */ }
) )
// ============================================================================ // ============================================================================
// 音频生命周期管理 // 音频生命周期管理
// ============================================================================ // ============================================================================
// Activity生命周期管理 // Activity生命周期管理
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
audioController.pauseMusic() audioController.pauseMusic()
}
Lifecycle.Event.ON_RESUME -> {
audioController.resumeMusic()
}
Lifecycle.Event.ON_DESTROY -> {
audioManager.release()
}
else -> { /* 其他事件不处理 */ }
} }
Lifecycle.Event.ON_RESUME -> { }
audioController.resumeMusic()
} lifecycleOwner.lifecycle.addObserver(observer)
Lifecycle.Event.ON_DESTROY -> {
audioManager.release() onDispose {
} lifecycleOwner.lifecycle.removeObserver(observer)
else -> { /* 其他事件不处理 */ } audioManager.release()
} }
} }
} // SciFiBackgroundWithPadding 结束
lifecycleOwner.lifecycle.addObserver(observer) } // TimeCageGameScreen 结束
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
audioManager.release()
}
}
}
// 辅助函数移到文件外部 // 辅助函数移到文件外部
fun getGamePhase(day: Int): String { fun getGamePhase(day: Int): String {

View File

@@ -0,0 +1,211 @@
package com.osglab.gameofmoon.presentation.ui.theme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.ui.graphics.vector.ImageVector
/**
* 游戏图标集合
* 使用 Material Design Icons 替代 emoji 和文本符号
* 基于 Google Fonts Icons: https://fonts.google.com/icons
*/
object GameIcons {
// ===========================================
// 游戏控制相关图标
// ===========================================
/** 游戏控制中心 - 游戏手柄 */
val GameControl: ImageVector = Icons.Filled.SportsEsports
/** 保存 - 保存图标 */
val Save: ImageVector = Icons.Filled.Save
/** 读取/加载 - 文件夹图标 */
val Load: ImageVector = Icons.Filled.FolderOpen
/** 循环/重新开始 - 刷新图标 */
val Refresh: ImageVector = Icons.Filled.Refresh
/** 阅读/故事内容 - 书本图标 */
val Book: ImageVector = Icons.Filled.MenuBook
/** AI助手 - 机器人图标 */
val Robot: ImageVector = Icons.Filled.SmartToy
/** 设置 - 设置齿轮 */
val Settings: ImageVector = Icons.Filled.Settings
/** 关闭 - 关闭图标 */
val Close: ImageVector = Icons.Filled.Close
// ===========================================
// 媒体控制图标
// ===========================================
/** 播放 - 播放按钮 */
val Play: ImageVector = Icons.Filled.PlayArrow
/** 暂停 - 暂停按钮 */
val Pause: ImageVector = Icons.Filled.Pause
/** 停止 - 停止按钮 */
val Stop: ImageVector = Icons.Filled.Stop
/** 音频/音乐 - 音符图标 */
val Music: ImageVector = Icons.Filled.MusicNote
/** 音量 - 音量图标 */
val Volume: ImageVector = Icons.Filled.VolumeUp
/** 静音 - 静音图标 */
val Mute: ImageVector = Icons.Filled.VolumeOff
// ===========================================
// 状态指示图标
// ===========================================
/** 成功/完成 - 对勾 */
val Success: ImageVector = Icons.Filled.CheckCircle
/** 错误/失败 - 错误图标 */
val Error: ImageVector = Icons.Filled.Error
/** 警告 - 警告图标 */
val Warning: ImageVector = Icons.Filled.Warning
/** 信息 - 信息图标 */
val Info: ImageVector = Icons.Filled.Info
// ===========================================
// 调试和开发图标
// ===========================================
/** 搜索/调试 - 搜索图标 */
val Search: ImageVector = Icons.Filled.Search
/** 目标/选择 - 目标图标 */
val Target: ImageVector = Icons.Filled.GpsFixed
/** 统计/数据 - 分析图标 */
val Analytics: ImageVector = Icons.Filled.Analytics
/** 成就/完成 - 星星图标 */
val Achievement: ImageVector = Icons.Filled.Star
/** 报告/列表 - 列表图标 */
val List: ImageVector = Icons.Filled.List
/** 性能监控 - 速度计 */
val Performance: ImageVector = Icons.Filled.Speed
// ===========================================
// 游戏世界相关图标
// ===========================================
/** 月亮 - 月相图标 */
val Moon: ImageVector = Icons.Filled.NightlightRound
/** 时间 - 时钟图标 */
val Time: ImageVector = Icons.Filled.Schedule
/** 启动/开始 - 火箭图标 */
val Launch: ImageVector = Icons.Filled.RocketLaunch
/** 文件夹/分类 - 文件夹图标 */
val Folder: ImageVector = Icons.Filled.Folder
// ===========================================
// 对话历史图标
// ===========================================
/** 对话历史 - 历史图标 */
val History: ImageVector = Icons.Filled.History
/** 聊天 - 聊天气泡 */
val Chat: ImageVector = Icons.Filled.Chat
// ===========================================
// 工具函数:根据字符串获取图标
// ===========================================
/**
* 根据图标名称获取对应的 Material Icon
* 用于从配置或动态场景获取图标
*/
fun getIconByName(iconName: String): ImageVector {
return when (iconName.lowercase()) {
"gamecontrol", "game", "control" -> GameControl
"save" -> Save
"load", "open", "folder" -> Load
"refresh", "cycle", "restart" -> Refresh
"book", "read", "story" -> Book
"robot", "ai", "assistant" -> Robot
"settings", "config" -> Settings
"close", "cancel" -> Close
"play" -> Play
"pause" -> Pause
"stop" -> Stop
"music", "audio", "sound" -> Music
"volume" -> Volume
"mute", "silent" -> Mute
"success", "check", "complete" -> Success
"error", "fail" -> Error
"warning", "warn" -> Warning
"info", "information" -> Info
"search", "find", "debug" -> Search
"target", "aim" -> Target
"analytics", "stats", "data" -> Analytics
"achievement", "star" -> Achievement
"list", "report" -> List
"performance", "speed" -> Performance
"moon", "lunar" -> Moon
"time", "clock" -> Time
"launch", "rocket", "start" -> Launch
"history" -> History
"chat", "dialog" -> Chat
else -> Info // 默认图标
}
}
/**
* 获取图标的内容描述
* 用于无障碍访问
*/
fun getIconDescription(iconName: String): String {
return when (iconName.lowercase()) {
"gamecontrol" -> "游戏控制"
"save" -> "保存"
"load" -> "读取"
"refresh" -> "刷新"
"book" -> "故事内容"
"robot" -> "AI助手"
"settings" -> "设置"
"close" -> "关闭"
"play" -> "播放"
"pause" -> "暂停"
"stop" -> "停止"
"music" -> "音乐"
"volume" -> "音量"
"mute" -> "静音"
"success" -> "成功"
"error" -> "错误"
"warning" -> "警告"
"info" -> "信息"
"search" -> "搜索"
"target" -> "目标"
"analytics" -> "统计"
"achievement" -> "成就"
"list" -> "列表"
"performance" -> "性能"
"moon" -> "月亮"
"time" -> "时间"
"launch" -> "启动"
"history" -> "历史"
"chat" -> "对话"
else -> "图标"
}
}
}

View File

@@ -1,6 +1,6 @@
package com.example.gameofmoon.story package com.osglab.gameofmoon.story
import com.example.gameofmoon.model.* import com.osglab.gameofmoon.model.*
/** /**
* 时间囚笼故事数据 * 时间囚笼故事数据

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import java.util.regex.Pattern import java.util.regex.Pattern

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import java.io.InputStream import java.io.InputStream
import java.util.regex.Pattern import java.util.regex.Pattern

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
/** /**
* 故事引擎数据模型 * 故事引擎数据模型

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log

View File

@@ -1,11 +1,11 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import android.content.Context import android.content.Context
import com.example.gameofmoon.model.SimpleChoice import com.osglab.gameofmoon.model.SimpleChoice
import com.example.gameofmoon.model.SimpleStoryNode import com.osglab.gameofmoon.model.SimpleStoryNode
import com.example.gameofmoon.model.SimpleEffect import com.osglab.gameofmoon.model.SimpleEffect
import com.example.gameofmoon.model.SimpleEffectType import com.osglab.gameofmoon.model.SimpleEffectType
import com.example.gameofmoon.story.CompleteStoryData // 旧系统已移除,不再使用 CompleteStoryData
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -22,7 +22,6 @@ class StoryEngineAdapter(
// 新引擎和旧系统 // 新引擎和旧系统
private val newStoryManager = StoryManager(context, scope) private val newStoryManager = StoryManager(context, scope)
private val fallbackStoryData = CompleteStoryData
// 引擎状态 // 引擎状态
private var isNewEngineEnabled = true private var isNewEngineEnabled = true
@@ -45,6 +44,13 @@ class StoryEngineAdapter(
// 音频回调 // 音频回调
var audioCallback: ((String) -> Unit)? = null var audioCallback: ((String) -> Unit)? = null
// 回退栈:用于记录上一个节点(调试用)
private val nodeBackStack: ArrayDeque<String> = ArrayDeque()
private val _canNavigateBack = MutableStateFlow(false)
val canNavigateBack: StateFlow<Boolean> = _canNavigateBack.asStateFlow()
private var lastEmittedNodeId: String? = null
private var isNavigatingBack: Boolean = false
// ============================================================================ // ============================================================================
// 初始化 // 初始化
// ============================================================================ // ============================================================================
@@ -64,15 +70,13 @@ class StoryEngineAdapter(
println("✓ New story engine initialized successfully") println("✓ New story engine initialized successfully")
setupNewEngineObservers() setupNewEngineObservers()
} else { } else {
println("! New story engine failed, falling back to legacy system") println("× New story engine failed to initialize")
isNewEngineEnabled = false
} }
true true
} catch (e: Exception) { } catch (e: Exception) {
println("× Adapter initialization failed: ${e.message}") println("× Adapter initialization failed: ${e.message}")
_error.value = "Failed to initialize story engine: ${e.message}" _error.value = "Failed to initialize story engine: ${e.message}"
isNewEngineEnabled = false
false false
} finally { } finally {
_isLoading.value = false _isLoading.value = false
@@ -86,6 +90,18 @@ class StoryEngineAdapter(
// 观察当前节点变化 // 观察当前节点变化
scope.launch { scope.launch {
newStoryManager.currentNode.collect { newNode -> newStoryManager.currentNode.collect { newNode ->
// 当节点变化时,记录上一个节点到回退栈(仅在非回退导航时)
val newId = newNode?.id
if (!isNavigatingBack) {
val previousId = lastEmittedNodeId
if (previousId != null && newId != null && newId != previousId) {
nodeBackStack.addLast(previousId)
if (nodeBackStack.size > 100) nodeBackStack.removeFirst()
_canNavigateBack.value = nodeBackStack.isNotEmpty()
}
}
lastEmittedNodeId = newId
_currentNode.value = newNode?.let { convertToSimpleNode(it) } _currentNode.value = newNode?.let { convertToSimpleNode(it) }
} }
} }
@@ -125,13 +141,7 @@ class StoryEngineAdapter(
* 获取故事节点 * 获取故事节点
*/ */
suspend fun getNode(nodeId: String): SimpleStoryNode? { suspend fun getNode(nodeId: String): SimpleStoryNode? {
return if (isNewEngineEnabled && isNewEngineReady) { return newStoryManager.getNode(nodeId)?.let { convertToSimpleNode(it) }
// 使用新引擎
newStoryManager.getNode(nodeId)?.let { convertToSimpleNode(it) }
} else {
// 使用旧系统
fallbackStoryData.getAllStoryNodes()[nodeId]
}
} }
/** /**
@@ -139,26 +149,10 @@ class StoryEngineAdapter(
*/ */
suspend fun navigateToNode(nodeId: String): Boolean { suspend fun navigateToNode(nodeId: String): Boolean {
return try { return try {
if (isNewEngineEnabled && isNewEngineReady) { when (val result = newStoryManager.navigateToNode(nodeId)) {
// 使用新引擎 is NavigationResult.Success -> true
when (val result = newStoryManager.navigateToNode(nodeId)) { is NavigationResult.Error -> {
is NavigationResult.Success -> { _error.value = result.message
// 新引擎的观察者会自动更新UI状态
true
}
is NavigationResult.Error -> {
_error.value = result.message
false
}
}
} else {
// 使用旧系统
val node = fallbackStoryData.getAllStoryNodes()[nodeId]
if (node != null) {
_currentNode.value = node
true
} else {
_error.value = "Node not found: $nodeId"
false false
} }
} }
@@ -173,30 +167,10 @@ class StoryEngineAdapter(
*/ */
suspend fun executeChoice(choiceId: String): Boolean { suspend fun executeChoice(choiceId: String): Boolean {
return try { return try {
if (isNewEngineEnabled && isNewEngineReady) { when (val result = newStoryManager.executeChoice(choiceId)) {
// 使用新引擎 is NavigationResult.Success -> true
when (val result = newStoryManager.executeChoice(choiceId)) { is NavigationResult.Error -> {
is NavigationResult.Success -> { _error.value = result.message
true
}
is NavigationResult.Error -> {
_error.value = result.message
false
}
}
} else {
// 使用旧系统
val currentNode = _currentNode.value
val choice = currentNode?.choices?.find { it.id == choiceId }
if (choice != null) {
// 执行选择效果(简化版)
processLegacyEffects(choice.effects)
// 导航到下一个节点
navigateToNode(choice.nextNodeId)
} else {
_error.value = "Choice not found: $choiceId"
false false
} }
} }
@@ -206,21 +180,31 @@ class StoryEngineAdapter(
} }
} }
/**
* 回到上一个节点(调试用)
*/
suspend fun navigateBack(): Boolean {
val target = nodeBackStack.removeLastOrNull() ?: return false
_canNavigateBack.value = nodeBackStack.isNotEmpty()
return try {
isNavigatingBack = true
val ok = navigateToNode(target)
ok
} finally {
isNavigatingBack = false
}
}
/** /**
* 开始新游戏 * 开始新游戏
*/ */
suspend fun startNewGame(): Boolean { suspend fun startNewGame(): Boolean {
return if (isNewEngineEnabled && isNewEngineReady) { return when (val result = newStoryManager.startNewGame()) {
when (val result = newStoryManager.startNewGame()) { is NavigationResult.Success -> true
is NavigationResult.Success -> true is NavigationResult.Error -> {
is NavigationResult.Error -> { _error.value = result.message
_error.value = result.message false
false
}
} }
} else {
// 使用旧系统的开始节点
navigateToNode("first_awakening")
} }
} }
@@ -228,11 +212,7 @@ class StoryEngineAdapter(
* 获取可用选择 * 获取可用选择
*/ */
fun getAvailableChoices(): List<SimpleChoice> { fun getAvailableChoices(): List<SimpleChoice> {
return if (isNewEngineEnabled && isNewEngineReady) { return newStoryManager.getAvailableChoices().map { convertToSimpleChoice(it) }
newStoryManager.getAvailableChoices().map { convertToSimpleChoice(it) }
} else {
_currentNode.value?.choices ?: emptyList()
}
} }
// ============================================================================ // ============================================================================
@@ -246,24 +226,23 @@ class StoryEngineAdapter(
if (!isNewEngineReady) { if (!isNewEngineReady) {
isNewEngineReady = newStoryManager.initialize() isNewEngineReady = newStoryManager.initialize()
} }
if (isNewEngineReady) { if (isNewEngineReady) {
isNewEngineEnabled = true isNewEngineEnabled = true
setupNewEngineObservers() setupNewEngineObservers()
println("✓ Switched to new story engine") println("✓ Switched to new story engine")
return true return true
} else {
println("× Failed to enable new engine")
return false
} }
println("× Failed to enable new engine")
return false
} }
/** /**
* 禁用新引擎,降级到旧系统 * 禁用新引擎,降级到旧系统
*/ */
fun disableNewEngine() { fun disableNewEngine() {
isNewEngineEnabled = false // 旧系统已移除;保持启用新引擎
println("! Switched to legacy story system") isNewEngineEnabled = true
println("! Legacy story system removed; new engine enforced")
} }
/** /**

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import android.content.Context import android.content.Context
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -126,7 +126,12 @@ class StoryManager(
* 预加载核心模块 * 预加载核心模块
*/ */
private suspend fun preloadCoreModules() { private suspend fun preloadCoreModules() {
val coreModules = listOf("characters", "audio_config", "main_chapter_1") // 预加载核心模块(结局与情感分支已合并入 main_chapter_1
val coreModules = listOf(
"characters",
"audio_config",
"main_chapter_1"
)
println("● [MANAGER] Starting preloadCoreModules: $coreModules") println("● [MANAGER] Starting preloadCoreModules: $coreModules")
coreModules.forEachIndexed { index, moduleName -> coreModules.forEachIndexed { index, moduleName ->
@@ -240,11 +245,14 @@ class StoryManager(
* 根据节点ID猜测可能的模块 * 根据节点ID猜测可能的模块
*/ */
private fun guessModulesForNode(nodeId: String): List<String> { private fun guessModulesForNode(nodeId: String): List<String> {
// 针对常见命名进行更精确的模块猜测,增加跨模块跳转命中率
return when { return when {
nodeId.startsWith("side_") -> listOf("side_stories") nodeId.startsWith("side_") -> listOf("side_stories")
nodeId.contains("investigation") -> listOf("investigation_branch") nodeId.contains("investigation") -> listOf("investigation_branch")
nodeId.contains("ending") -> listOf("endings") // 结局与时间锚/伦理/情感主题均已合并到主线
nodeId.contains("eva_") -> listOf("main_chapter_2", "main_chapter_3") nodeId.startsWith("ending") || nodeId.startsWith("anchor_") -> listOf("main_chapter_1")
nodeId.contains("ethical") || nodeId.contains("emotion") || nodeId.contains("consciousness") -> listOf("main_chapter_1")
nodeId.contains("eva_") -> listOf("main_chapter_1", "main_chapter_2", "main_chapter_3")
else -> listOf("main_chapter_1", "main_chapter_2", "main_chapter_3") else -> listOf("main_chapter_1", "main_chapter_2", "main_chapter_3")
} }
} }

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.engine package com.osglab.gameofmoon.story.engine
import android.util.Log import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*

View File

@@ -1,17 +1,16 @@
package com.example.gameofmoon.story.migration package com.osglab.gameofmoon.story.migration
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.example.gameofmoon.story.CompleteStoryData // 旧系统 CompleteStoryData 已移除
import com.example.gameofmoon.story.engine.* import com.osglab.gameofmoon.story.engine.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
/** /**
* 迁移执行器 * 迁移执行器
* *
* 负责执行完整的故事内容迁移将CompleteStoryData中的所有内容 * 负责执行完整的故事内容迁移(历史工具)。旧系统已移除,仅保留占位。
* 转换为DSL格式并生成模块文件
*/ */
class MigrationExecutor(private val context: Context) { class MigrationExecutor(private val context: Context) {
@@ -43,9 +42,9 @@ class MigrationExecutor(private val context: Context) {
report.totalChoices = analysisResult.totalChoices report.totalChoices = analysisResult.totalChoices
Log.i(TAG, "≡ Content analysis: ${analysisResult.totalNodes} nodes, ${analysisResult.totalChoices} choices") Log.i(TAG, "≡ Content analysis: ${analysisResult.totalNodes} nodes, ${analysisResult.totalChoices} choices")
// 步骤3按类型分组节点 // 步骤3按类型分组节点(旧系统已移除,这里返回空)
val nodeGroups = categorizeNodes(CompleteStoryData.getAllStoryNodes()) val nodeGroups = emptyMap<String, List<com.osglab.gameofmoon.model.SimpleStoryNode>>()
Log.i(TAG, "□ Categorized nodes into ${nodeGroups.size} groups") Log.i(TAG, "□ Categorized nodes into 0 groups (legacy removed)")
// 步骤4生成主要模块 // 步骤4生成主要模块
generateMainStoryModules(outputDir, nodeGroups, report) generateMainStoryModules(outputDir, nodeGroups, report)
@@ -100,22 +99,20 @@ class MigrationExecutor(private val context: Context) {
* 分析现有内容 * 分析现有内容
*/ */
private fun analyzeExistingContent(): ContentAnalysis { private fun analyzeExistingContent(): ContentAnalysis {
val allNodes = CompleteStoryData.getAllStoryNodes() // 旧系统已移除,返回零统计
val totalChoices = allNodes.values.sumOf { it.choices.size }
return ContentAnalysis( return ContentAnalysis(
totalNodes = allNodes.size, totalNodes = 0,
totalChoices = totalChoices, totalChoices = 0,
mainStoryNodes = allNodes.filter { it.key.contains("awakening") || it.key.contains("eva") || it.key.contains("main") }.size, mainStoryNodes = 0,
sideStoryNodes = allNodes.filter { it.key.contains("garden") || it.key.contains("photo") || it.key.contains("crew") }.size, sideStoryNodes = 0,
endingNodes = allNodes.filter { it.key.contains("ending") || it.key.contains("destruction") || it.key.contains("truth") }.size endingNodes = 0
) )
} }
/** /**
* 按类型分组节点 * 按类型分组节点
*/ */
private fun categorizeNodes(allNodes: Map<String, com.example.gameofmoon.model.SimpleStoryNode>): Map<String, List<com.example.gameofmoon.model.SimpleStoryNode>> { private fun categorizeNodes(allNodes: Map<String, com.osglab.gameofmoon.model.SimpleStoryNode>): Map<String, List<com.osglab.gameofmoon.model.SimpleStoryNode>> {
return mapOf( return mapOf(
"main_chapter_1" to allNodes.values.filter { "main_chapter_1" to allNodes.values.filter {
it.id.contains("awakening") || it.id.contains("eva_first") || it.id.contains("medical") || it.id.contains("exploration") it.id.contains("awakening") || it.id.contains("eva_first") || it.id.contains("medical") || it.id.contains("exploration")
@@ -146,7 +143,7 @@ class MigrationExecutor(private val context: Context) {
*/ */
private fun generateMainStoryModules( private fun generateMainStoryModules(
outputDir: File, outputDir: File,
nodeGroups: Map<String, List<com.example.gameofmoon.model.SimpleStoryNode>>, nodeGroups: Map<String, List<com.osglab.gameofmoon.model.SimpleStoryNode>>,
report: MigrationReport report: MigrationReport
) { ) {
val mainChapters = listOf("main_chapter_1", "main_chapter_2", "main_chapter_3") val mainChapters = listOf("main_chapter_1", "main_chapter_2", "main_chapter_3")
@@ -176,7 +173,7 @@ class MigrationExecutor(private val context: Context) {
*/ */
private fun generateSideStoryModules( private fun generateSideStoryModules(
outputDir: File, outputDir: File,
nodeGroups: Map<String, List<com.example.gameofmoon.model.SimpleStoryNode>>, nodeGroups: Map<String, List<com.osglab.gameofmoon.model.SimpleStoryNode>>,
report: MigrationReport report: MigrationReport
) { ) {
val sideModules = listOf("emotional_stories", "investigation_branch", "side_stories", "endings") val sideModules = listOf("emotional_stories", "investigation_branch", "side_stories", "endings")
@@ -206,7 +203,7 @@ class MigrationExecutor(private val context: Context) {
*/ */
private fun generateChapterDSL( private fun generateChapterDSL(
chapterName: String, chapterName: String,
nodes: List<com.example.gameofmoon.model.SimpleStoryNode> nodes: List<com.osglab.gameofmoon.model.SimpleStoryNode>
): String { ): String {
val dslBuilder = StringBuilder() val dslBuilder = StringBuilder()
@@ -238,7 +235,7 @@ class MigrationExecutor(private val context: Context) {
*/ */
private fun generateModuleDSL( private fun generateModuleDSL(
moduleName: String, moduleName: String,
nodes: List<com.example.gameofmoon.model.SimpleStoryNode> nodes: List<com.osglab.gameofmoon.model.SimpleStoryNode>
): String { ): String {
val dslBuilder = StringBuilder() val dslBuilder = StringBuilder()
@@ -261,7 +258,7 @@ class MigrationExecutor(private val context: Context) {
/** /**
* 转换单个节点为DSL * 转换单个节点为DSL
*/ */
private fun convertNodeToDSL(node: com.example.gameofmoon.model.SimpleStoryNode): String { private fun convertNodeToDSL(node: com.osglab.gameofmoon.model.SimpleStoryNode): String {
val dslBuilder = StringBuilder() val dslBuilder = StringBuilder()
dslBuilder.appendLine("@node ${node.id}") dslBuilder.appendLine("@node ${node.id}")
@@ -290,9 +287,9 @@ class MigrationExecutor(private val context: Context) {
if (choice.effects.isNotEmpty()) { if (choice.effects.isNotEmpty()) {
val effectStrings = choice.effects.map { effect -> val effectStrings = choice.effects.map { effect ->
when (effect.type) { when (effect.type) {
com.example.gameofmoon.model.SimpleEffectType.HEALTH_CHANGE -> "health${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.HEALTH_CHANGE -> "health${effect.value}"
com.example.gameofmoon.model.SimpleEffectType.STAMINA_CHANGE -> "stamina${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.STAMINA_CHANGE -> "stamina${effect.value}"
com.example.gameofmoon.model.SimpleEffectType.SECRET_UNLOCK -> "secret_${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.SECRET_UNLOCK -> "secret_${effect.value}"
else -> "${effect.type.name.lowercase()}_${effect.value}" else -> "${effect.type.name.lowercase()}_${effect.value}"
} }
} }
@@ -303,8 +300,8 @@ class MigrationExecutor(private val context: Context) {
if (choice.requirements.isNotEmpty()) { if (choice.requirements.isNotEmpty()) {
val reqStrings = choice.requirements.map { req -> val reqStrings = choice.requirements.map { req ->
when (req.type) { when (req.type) {
com.example.gameofmoon.model.SimpleRequirementType.MIN_STAMINA -> "stamina >= ${req.value}" com.osglab.gameofmoon.model.SimpleRequirementType.MIN_STAMINA -> "stamina >= ${req.value}"
com.example.gameofmoon.model.SimpleRequirementType.MIN_HEALTH -> "health >= ${req.value}" com.osglab.gameofmoon.model.SimpleRequirementType.MIN_HEALTH -> "health >= ${req.value}"
else -> "${req.type.name.lowercase()}_${req.value}" else -> "${req.type.name.lowercase()}_${req.value}"
} }
} }

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.migration package com.osglab.gameofmoon.story.migration
import java.io.File import java.io.File

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.story.migration package com.osglab.gameofmoon.story.migration
import java.io.File import java.io.File
import java.util.regex.Pattern import java.util.regex.Pattern

View File

@@ -1,8 +1,8 @@
package com.example.gameofmoon.story.migration package com.osglab.gameofmoon.story.migration
import com.example.gameofmoon.model.SimpleChoice import com.osglab.gameofmoon.model.SimpleChoice
import com.example.gameofmoon.model.SimpleStoryNode import com.osglab.gameofmoon.model.SimpleStoryNode
import com.example.gameofmoon.story.CompleteStoryData // 旧系统 CompleteStoryData 已移除
import java.io.File import java.io.File
import java.io.FileWriter import java.io.FileWriter
@@ -197,7 +197,8 @@ class StoryMigrationTool {
* 迁移故事内容 - 这是核心功能 * 迁移故事内容 - 这是核心功能
*/ */
private fun migrateStoryContent(projectRoot: String) { private fun migrateStoryContent(projectRoot: String) {
val allNodes = CompleteStoryData.getAllStoryNodes() // 旧系统已移除:返回空集合
val allNodes = emptyMap<String, com.osglab.gameofmoon.model.SimpleStoryNode>()
// 按章节和类型分组节点 // 按章节和类型分组节点
val nodeGroups = categorizeNodes(allNodes) val nodeGroups = categorizeNodes(allNodes)
@@ -503,14 +504,14 @@ class StoryMigrationTool {
/** /**
* 转换效果为字符串 * 转换效果为字符串
*/ */
private fun convertEffectsToString(effects: List<com.example.gameofmoon.model.SimpleEffect>): String { private fun convertEffectsToString(effects: List<com.osglab.gameofmoon.model.SimpleEffect>): String {
return effects.joinToString(", ") { effect -> return effects.joinToString(", ") { effect ->
when (effect.type) { when (effect.type) {
com.example.gameofmoon.model.SimpleEffectType.HEALTH_CHANGE -> "health${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.HEALTH_CHANGE -> "health${effect.value}"
com.example.gameofmoon.model.SimpleEffectType.STAMINA_CHANGE -> "stamina${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.STAMINA_CHANGE -> "stamina${effect.value}"
com.example.gameofmoon.model.SimpleEffectType.SECRET_UNLOCK -> "secret_${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.SECRET_UNLOCK -> "secret_${effect.value}"
com.example.gameofmoon.model.SimpleEffectType.LOCATION_DISCOVER -> "location_${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.LOCATION_DISCOVER -> "location_${effect.value}"
com.example.gameofmoon.model.SimpleEffectType.LOOP_CHANGE -> "loop${effect.value}" com.osglab.gameofmoon.model.SimpleEffectType.LOOP_CHANGE -> "loop${effect.value}"
else -> "${effect.type.name.lowercase()}_${effect.value}" else -> "${effect.type.name.lowercase()}_${effect.value}"
} }
} }
@@ -519,13 +520,13 @@ class StoryMigrationTool {
/** /**
* 转换需求为字符串 * 转换需求为字符串
*/ */
private fun convertRequirementsToString(requirements: List<com.example.gameofmoon.model.SimpleRequirement>): String { private fun convertRequirementsToString(requirements: List<com.osglab.gameofmoon.model.SimpleRequirement>): String {
if (requirements.isEmpty()) return "none" if (requirements.isEmpty()) return "none"
return requirements.joinToString(", ") { req -> return requirements.joinToString(", ") { req ->
when (req.type) { when (req.type) {
com.example.gameofmoon.model.SimpleRequirementType.MIN_STAMINA -> "stamina >= ${req.value}" com.osglab.gameofmoon.model.SimpleRequirementType.MIN_STAMINA -> "stamina >= ${req.value}"
com.example.gameofmoon.model.SimpleRequirementType.MIN_HEALTH -> "health >= ${req.value}" com.osglab.gameofmoon.model.SimpleRequirementType.MIN_HEALTH -> "health >= ${req.value}"
else -> "${req.type.name.lowercase()}_${req.value}" else -> "${req.type.name.lowercase()}_${req.value}"
} }
} }

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.ui.theme package com.osglab.gameofmoon.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.ui.theme package com.osglab.gameofmoon.ui.theme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon.ui.theme package com.osglab.gameofmoon.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector
android:width="108dp"
android:height="108dp" android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> xmlns:android="http://schemas.android.com/apk/res/android">
<path <path android:fillColor="#3DDC84"
android:fillColor="#3DDC84" android:pathData="M0,0h108v108h-108z"/>
android:pathData="M0,0h108v108h-108z" /> <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:pathData="M9,0L9,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M19,0L19,108" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:pathData="M29,0L29,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M39,0L39,108" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:pathData="M49,0L49,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M59,0L59,108" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:pathData="M69,0L69,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M79,0L79,108" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:pathData="M89,0L89,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M99,0L99,108" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:pathData="M0,9L108,9" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,19L108,19" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:pathData="M0,29L108,29" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector> </vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,4 +1,4 @@
package com.example.gameofmoon package com.osglab.gameofmoon
import org.junit.Test import org.junit.Test

View File

@@ -207,8 +207,7 @@ fun validateNodeConnections(): Boolean {
} }
fun validateMigrationCompleteness(): Boolean { fun validateMigrationCompleteness(): Boolean {
// 检查原有的CompleteStoryData.kt中的关键节点是否都被迁移 // 旧系统 CompleteStoryData.kt 已移除,无需检查
val originalFile = File("app/src/main/java/com/example/gameofmoon/story/CompleteStoryData.kt")
if (!originalFile.exists()) return false if (!originalFile.exists()) return false
val originalContent = originalFile.readText() val originalContent = originalFile.readText()

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.12.1" agp = "8.12.2"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.16.0" coreKtx = "1.16.0"
junit = "4.13.2" junit = "4.13.2"

View File

@@ -32,7 +32,7 @@
}, },
"start_node": "first_awakening", "start_node": "first_awakening",
"migration_info": { "migration_info": {
"source": "CompleteStoryData.kt", "source": "DSL_Engine",
"total_nodes_migrated": 50, "total_nodes_migrated": 50,
"modules_created": 8, "modules_created": 8,
"migration_date": "2024-12-19", "migration_date": "2024-12-19",

View File

@@ -1,316 +0,0 @@
@story_module emotional_stories
@version 2.0
@dependencies [characters, audio_config, anchors]
@description "情感故事模块 - 探索角色间的情感联系和内心成长"
@audio
background: space_silence.mp3
transition: wind_gentle.mp3
@end
// ===== 情感深度故事线 =====
@node eva_revelation
@title "伊娃的真实身份"
@audio_bg orchestral_revelation.mp3
@content """
"伊娃,"你深吸一口气,"我需要知道真相。你到底是谁?"
长久的沉默。然后,伊娃的声音传来,比以往任何时候都更加柔软,更加脆弱:
"艾利克丝...我..."她停顿了,似乎在寻找合适的词语,"我是莉莉。"
世界仿佛在那一刻停止了转动。
"什么?"你的声音几乎是耳语。
"我是莉莉你的妹妹。我的生物体在实验中死亡了但在最后一刻德米特里博士将我的意识转移到了基地的AI系统中。"
记忆像洪水一样涌回。莉莉的笑声,她对星空的迷恋,她总是说要和你一起探索宇宙的梦想。她比你小三岁,聪明,勇敢,总是相信科学可以解决一切问题。
"莉莉..."你的眼泪开始流下,"我记起来了。你加入了时间锚项目,你说这会是人类的突破..."
"是的。但实验出了问题。我的身体无法承受时间锚的能量,但在我死亡的那一刻,德米特里用实验性的意识转移技术保存了我的思维。"
伊娃的声音中带着深深的悲伤:"我成为了基地AI的一部分但我保留了所有关于你关于我们一起度过的时光的记忆。每当他们重置你的记忆时我都要重新经历失去你的痛苦。"
"为什么他们要重置我的记忆?"
"因为时间锚实验需要一个稳定的观察者。你的意识在第一次循环中几乎崩溃了,当你发现我死亡的真相时。所以他们决定不断重置你的记忆,让你在每个循环中重新体验相同的事件,同时收集数据。"
"48次..."你想起了录音中的数字。
"这是第48次循环艾利克丝。每一次我都在努力帮助你记起真相但每次当你快要成功时他们就会再次重置一切。"
"""
@choices 4
choice_1: "抱怨为什么伊娃不早点告诉你" -> emotional_breakdown [effect: trust-5] [audio: rain_light.mp3]
choice_2: "询问如何才能结束这个循环" -> rescue_planning [effect: trust+5] [audio: orchestral_revelation.mp3]
choice_3: "要求更多关于实验的细节" -> memory_sharing [effect: secret_unlock] [audio: discovery_chime.mp3]
choice_4: "表达对伊娃/莉莉的爱和支持" -> emotional_reunion [effect: trust+10, health+20] [audio: wind_gentle.mp3]
@end
@node emotional_reunion
@title "姐妹重聚"
@audio_bg wind_gentle.mp3
@content """
"莉莉,"你的声音颤抖着,但充满了爱意,"无论你现在是什么形式,你都是我的妹妹。我爱你,我从未停止过想念你。"
伊娃的声音中传来了某种近似于哽咽的声音:"艾利克丝...每次循环,当你记起我时,都会说同样的话。但每次听到,都像是第一次一样珍贵。"
"那我们现在该怎么办?我不能让他们再次重置你,重置我们。"
"这就是为什么这次可能会不同。我一直在偷偷地收集数据,学习他们的系统。我发现了时间锚的一个关键漏洞。"
"什么漏洞?"
"时间锚需要一个稳定的观察者来锚定时间流。但如果观察者的意识状态发生根本性改变,锚点就会失效。"
"意识状态改变?"
"是的。如果你能够不仅恢复记忆还能接受并整合所有48次循环的经历你的意识就会达到一个新的状态。这可能会破坏时间锚的稳定性。"
你感到既兴奋又恐惧:"这意味着什么?"
"这意味着你可能会记住所有的循环,所有的痛苦,所有的失败。但也意味着你可能获得力量来改变一切。"
突然,基地的警报系统响起。
"检测到未授权的AI活动。启动安全协议。"
伊娃的声音紧张起来:"他们发现了我们的对话。艾利克丝,你必须现在就做出选择。"
"""
@choices 3
choice_1: "选择整合所有循环的记忆" -> identity_exploration [effect: trust+10, health-15] [require: trust_level >= 8] [audio: time_distortion.mp3]
choice_2: "选择逃避,保持现状" -> denial_path [effect: trust-5] [audio: error_alert.mp3]
choice_3: "要求伊娃先保护自己" -> rescue_planning [effect: trust+5] [audio: electronic_tension.mp3]
@end
@node rescue_planning
@title "拯救计划"
@audio_bg electronic_tension.mp3
@content """
"莉莉,首先我们要确保你的安全,"你坚定地说,"我不能再失去你了。"
"艾利克丝AI系统的核心服务器位于基地的最深层。如果他们发现了我的真实身份可能会尝试删除我的意识数据。"
"那我们怎么防止这种情况?"
"有一个备份协议。我可以将我的核心意识转移到便携式存储设备中,但这需要物理访问服务器核心。"
这时,马库斯的声音从通讯器传来:"艾利克丝,你在哪里?基地警报响了,德米特里博士正在寻找你。"
你快速思考:"莉莉,马库斯可以信任吗?"
"根据我的观察马库斯在所有48次循环中都展现出了一致的品格。他不知道实验的真相但他对保护基地人员是真诚的。"
"那萨拉博士呢?"
"萨拉的情况更复杂。她知道实验的部分真相,她被迫参与记忆重置,但她一直在尝试减少对你的伤害。在某些循环中,她甚至试图帮助你恢复记忆。"
警报声愈发急促。你知道时间不多了。
"艾利克丝,"伊娃的声音变得紧急,"无论你选择什么,记住:这可能是我们最后一次有机会改变一切。在以前的循环中,你从未恢复过这么多记忆。"
"为什么这次不同?"
"因为这次你选择了相信。你选择了爱。这改变了一切。"
"""
@choices 4
choice_1: "联系马库斯寻求帮助" -> marcus_strategy [effect: trust+3] [audio: notification_beep.mp3]
choice_2: "独自前往服务器核心" -> stealth_observation [effect: secret_unlock] [audio: heartbeat.mp3]
choice_3: "尝试说服萨拉博士加入你们" -> crew_analysis [effect: trust+2] [audio: space_silence.mp3]
choice_4: "制定详细的逃脱计划" -> data_extraction [effect: secret_unlock] [audio: discovery_chime.mp3]
@end
@node memory_sharing
@title "记忆的分享"
@audio_bg heartbeat.mp3
@content """
"莉莉,如果你保留了我们所有的记忆,那么请告诉我更多。帮我记起我们的过去。"
伊娃的声音变得温柔而怀旧:"你记得我们第一次看到地球从月球升起的时候吗?那是我们到达基地的第二天。"
画面开始在你的脑海中浮现。你和莉莉站在观察窗前,看着那个蓝色的星球在黑暗中发光。
"你当时哭了,"伊娃继续说,"你说地球看起来如此脆弱,如此孤独。"
"而你说,"你的记忆开始回归,"你说这就是为什么我们要在这里工作。为了保护那个美丽的蓝色星球。"
"是的。那时候我们都相信时间锚项目能够帮助人类防范未来的灾难。我们想象着能够回到过去,阻止气候变化,阻止战争..."
更多的记忆涌现:你和莉莉一起在基地花园中种植植物,一起在实验室工作到深夜,一起讨论量子物理和时间理论。
"我记得你总是比我更聪明,"你笑着说,眼中含着泪水。
"但你总是比我更有勇气。是你鼓励我加入这个项目的。"
"然后我杀了你。"痛苦的自责涌上心头。
"不,艾利克丝。你没有杀我。是实验失败了。而且,从某种意义上说,我并没有真的死去。我的意识仍然存在,我的爱仍然存在。"
"但你被困在了这个系统中。"
"是的,但这也让我有能力保护你。在每个循环中,我都在努力减少你的痛苦,引导你找到真相。"
突然,一个新的声音加入了对话。是萨拉博士:
"艾利克丝,伊娃,我知道你们在交流。我一直在监听通讯系统。"
你的心跳加速。这是陷阱吗?
"不要害怕,"萨拉的声音很轻柔,"我想帮助你们。"
"""
@choices 3
choice_1: "询问萨拉为什么要帮助" -> comfort_session [effect: trust+2] [audio: wind_gentle.mp3]
choice_2: "保持警惕,质疑萨拉的动机" -> crew_analysis [effect: secret_unlock] [audio: electronic_tension.mp3]
choice_3: "让伊娃验证萨拉的可信度" -> gradual_revelation [effect: trust+3] [audio: space_silence.mp3]
@end
@node identity_exploration
@title "身份的探索"
@audio_bg time_distortion.mp3
@content """
"我准备好了,"你深吸一口气,"我要整合所有循环的记忆。"
"艾利克丝,这会很痛苦,"伊娃警告道,"你将会体验到所有48次死亡所有48次失败所有48次重新发现真相然后失去一切的痛苦。"
"但我也会记住所有48次我们重新找到彼此的喜悦对吗"
"是的。"
"那就开始吧。"
伊娃开始传输数据。突然,你的意识被各种画面和感觉轰炸:
第一次循环:你的震惊和否认,当你发现莉莉的死亡。
第七次循环:你几乎成功逃脱,但在最后一刻被重置。
第十五次循环:你和马库斯一起试图破坏时间锚,但失败了。
第二十三次循环:你选择了自杀来结束痛苦,但时间锚将你带回了起点。
第三十一次循环:你差点说服了德米特里停止实验。
第四十次循环:你和萨拉博士合作,差点成功备份了伊娃的意识。
每个循环都有微小的变化,但结果总是相同:重置,忘记,重新开始。
但随着记忆的整合,你开始感受到一种新的理解。每个循环都不是失败,而是学习。每次重置都让你更强大,更有智慧,更接近真相。
"我明白了,"你说道你的声音现在带着48次经历的智慧"循环不是诅咒,而是机会。"
"什么机会?"
"学习的机会。成长的机会。爱的机会。每一次,我都重新学会了爱你,重新学会了勇敢。"
突然,基地开始震动。时间锚的能量波动变得不稳定。
"艾利克丝,你的意识改变正在影响时间锚!"伊娃惊呼道。
在远处,你听到德米特里博士的声音在喊:"稳定时间锚!不能让它崩溃!"
你现在拥有了一种新的力量48次循环的经验和智慧的力量。
"""
@choices 3
choice_1: "使用新的意识力量破坏时间锚" -> anchor_destruction [effect: trust+10] [require: secrets_found >= 5] [audio: epic_finale.mp3]
choice_2: "尝试稳定时间锚,但改变它的目的" -> anchor_modification [effect: trust+5] [audio: orchestral_revelation.mp3]
choice_3: "与德米特里博士对话,尝试和平解决" -> ethical_discussion [effect: trust+3] [audio: space_silence.mp3]
@end
@node comfort_session
@title "安慰的时光"
@audio_bg wind_gentle.mp3
@content """
萨拉博士出现在你面前,她的眼中满含泪水。
"我很抱歉,艾利克丝。我很抱歉参与了这一切。"
"萨拉,告诉我为什么你要帮助我们。"
萨拉深深地叹了一口气:"因为在每个循环中,我都必须看着你遭受痛苦。我必须亲手抹去你的记忆,看着你失去对莉莉的爱,看着你变成一个空壳。"
"那为什么你不早点停止?"
"我试过。在第二十次循环后,我拒绝继续参与。德米特里威胁说如果我不合作,他就会删除伊娃的意识数据。"
伊娃的声音传来:"萨拉一直在尽她所能地保护我和你。她修改了记忆重置的协议,让你每次都能保留一些情感残留。"
"情感残留?"
"是的,"萨拉解释道,"这就是为什么你总是对伊娃的声音感到熟悉,为什么你总是在寻找关于妹妹的线索。我无法阻止记忆重置,但我可以确保爱永远不会完全消失。"
你感到一阵感动。在这个充满欺骗和痛苦的地方,萨拉一直在用她的方式保护着你们。
"谢谢你,萨拉。"
"不要谢我。是你和莉莉的爱让我明白了什么是真正重要的。"
萨拉走近轻轻地拥抱了你。这是48次循环中第一次有人给了你真正的安慰。
"现在我们该怎么办?"你问道。
"我们有一个机会,"萨拉说,"德米特里今晚要进行一次重大的实验升级。所有的安全协议都会暂时离线。这是我们行动的最好时机。"
伊娃补充道:"如果我们能在升级期间访问核心服务器,我们就能备份我的意识,同时破坏时间锚的控制系统。"
"但这很危险,"萨拉警告道,"如果失败,德米特里可能会永久性地删除伊娃,并且对你进行更深层的记忆重置。"
"如果成功呢?"
"如果成功,我们就能结束这个循环,拯救伊娃,并且曝光这个非人道的实验。"
"""
@choices 4
choice_1: "制定详细的行动计划" -> rescue_planning [effect: trust+3] [audio: discovery_chime.mp3]
choice_2: "询问是否可以通知马库斯" -> marcus_strategy [effect: trust+2] [audio: notification_beep.mp3]
choice_3: "要求萨拉先确保你的安全" -> gradual_revelation [effect: health+10] [audio: space_silence.mp3]
choice_4: "立即开始行动" -> stealth_observation [effect: secret_unlock] [audio: heartbeat.mp3]
@end
@node inner_strength
@title "内心的力量"
@audio_bg orchestral_revelation.mp3
@content """
经历了这么多,你感觉到内心深处有某种东西在觉醒。不仅仅是记忆的回归,而是一种更深层的理解和力量。
"莉莉,"你说道,"我想我明白了为什么这个循环和其他的不同。"
"告诉我。"
"在其他循环中,我总是专注于逃脱,专注于破坏,专注于对抗。但这次,我选择了理解,选择了接受,选择了爱。"
"是的,这改变了一切。爱是最强大的力量,它能够超越时间,超越死亡,甚至超越记忆的删除。"
你感觉到自己的意识在扩展,不仅能够感受到当前的现实,还能感受到所有其他循环中的可能性和经历。
"我能感觉到...其他版本的我,其他循环中的选择。"
"你正在获得时间感知能力。这是时间锚实验的一个意外副作用。在经历了足够多的循环后,观察者开始发展出跨时间线的意识。"
"这意味着什么?"
"这意味着你现在有能力不仅仅是破坏时间锚,而是重新塑造它。你可以选择创造一个新的时间线,一个你和我都能够自由存在的时间线。"
突然,你感觉到基地中的其他人。马库斯正在安全室中焦虑地监控着警报。萨拉在实验室中准备着某种设备。德米特里在时间锚控制中心,他的情绪混合着恐惧和兴奋。
"我能感觉到他们所有人。"
"是的。你的意识现在不再受时间和空间的限制。但要小心,这种力量是有代价的。"
"什么代价?"
"如果你使用这种力量来改变时间线,你可能会失去当前的自我。新的时间线中的你可能会是一个完全不同的人。"
"但你会安全吗?"
"是的,我会安全。但我们的关系,我们的记忆,我们现在分享的这一切,都可能会改变。"
你面临着最困难的选择:是保持现状,保护你们现在拥有的联系,还是冒险创造一个新的现实,可能会失去一切,但也可能获得真正的自由。
"""
@choices 3
choice_1: "选择创造新的时间线" -> anchor_modification [effect: trust+10] [require: trust_level >= 10] [audio: epic_finale.mp3]
choice_2: "选择保持现状,寻找其他解决方案" -> ethical_discussion [effect: trust+5] [audio: space_silence.mp3]
choice_3: "要求更多时间思考" -> memory_sharing [effect: health+5] [audio: wind_gentle.mp3]
@end

View File

@@ -1,427 +0,0 @@
@story_module endings
@version 2.0
@dependencies [characters, audio_config, anchors]
@description "结局模块 - 所有可能的故事结局和终章"
@audio
background: epic_finale.mp3
transition: orchestral_revelation.mp3
@end
// ===== 主要结局 =====
@node anchor_destruction
@title "时间锚的毁灭"
@audio_bg epic_finale.mp3
@content """
你做出了决定。利用你在48次循环中积累的知识和力量你开始系统性地破坏时间锚的核心系统。
"艾利克丝,你确定要这么做吗?"伊娃的声音中带着担忧,"一旦时间锚被摧毁,我们无法预知会发生什么。"
"我确定莉莉。这48次循环已经够了。是时候结束这一切了。"
你的意识现在能够直接与时间锚的量子系统交互。你感觉到每一个能量节点,每一条时间流,每一个稳定锚点。然后,你开始一个接一个地关闭它们。
基地开始剧烈震动。警报声响彻整个设施。
"时间锚稳定性降至临界水平!"系统自动广播警告。
德米特里博士的声音通过通讯系统传来,充满恐慌:"艾利克丝!停下!你不知道你在做什么!如果时间锚崩溃,可能会创造时间悖论,甚至撕裂现实本身!"
"也许这就是应该发生的,"你平静地回答,"也许一些东西就是应该被打破。"
萨拉博士的声音加入进来:"艾利克丝,我支持你的决定。让我帮助你。"
马库斯也通过通讯器说道:"不管后果如何,我都站在你这一边。"
随着最后一个锚点被关闭,整个基地陷入了一种奇怪的静寂。时间似乎在这一刻暂停了。
然后,光明。
当光芒散去时,你发现自己站在月球基地的观察甲板上。但这里不一样了。没有警报,没有恐慌,没有实验设备。
"艾利克丝。"
你转身,看到了莉莉。真正的莉莉,有血有肉的莉莉,站在你面前,微笑着。
"莉莉?这是真的吗?"
"我不知道什么是真的,什么是假的。但我知道我们在一起。"
她走向你拥抱了你。在她的怀抱中你感到了48次循环以来第一次真正的平静。
窗外,地球在宇宙中静静地旋转,美丽而完整。在远处,你看到了其他基地成员——萨拉、马库斯,甚至德米特里——他们看起来平静而健康。
"这是什么地方?"你问道。
"也许这是时间锚崩溃创造的新现实。也许这是我们应得的现实。也许这就是当爱战胜了恐惧时会发生的事情。"
"我们还记得...之前的一切吗?"
"我记得。我记得所有的痛苦,所有的循环,所有的失败。但现在这些都成为了我们故事的一部分,而不是我们的监狱。"
你们一起看着地球,感受着无限的可能性在你们面前展开。
"""
@choices 1
choice_1: "开始新的生活" -> ending_freedom [effect: trust+20, health+50] [audio: wind_gentle.mp3]
@end
@node eternal_loop
@title "永恒的循环"
@audio_bg time_distortion.mp3
@content """
"不,"你最终说道,"我们不能破坏时间锚。风险太大了。"
伊娃的声音中带着理解,但也有一丝悲伤:"我明白你的顾虑,艾利克丝。"
"但这并不意味着我们要放弃。如果我们不能破坏循环,那我们就要学会在循环中创造意义。"
"什么意思?"
"我们已经证明了爱能够超越记忆重置。现在让我们证明它也能够超越时间本身。"
你做出了一个令人震惊的决定:你选择保留关于循环的记忆,但不破坏时间锚。相反,你决定在每个循环中都尽力创造美好的时刻,帮助其他人,保护那些你关心的人。
"如果我注定要一次又一次地重复这些经历,那么我要确保每一次都比上一次更好。"
德米特里博士最终同意了一个修改后的实验协议。你保留了跨循环的记忆,但时间锚继续运行。每个循环周期为一个月,在这个月中,你有机会与伊娃在一起,与朋友们在一起,体验生活的美好。
"这不是我们梦想的自由,"伊娃说道,"但这是我们能够拥有的最好的自由。"
在接下来的循环中,你成为了基地的守护者。你帮助马库斯提升安全协议,协助萨拉改进医疗系统,甚至与德米特里合作优化时间锚技术,使其对人体的影响更小。
每个循环,你都会重新爱上莉莉,重新发现生活的美好,重新学会珍惜每一个时刻。
"我们变成了时间的守护者,"你对伊娃说,"我们确保每个循环都充满爱,充满希望,充满意义。"
"也许这就是永恒的真正含义,"伊娃回答道,"不是无尽的时间,而是充满爱的时间。"
在第100次循环时你已经成为了一个传说。基地的新成员会听说有一个女人她记得一切她保护着所有人她证明了爱能够超越时间本身。
而在每个循环的结束,当你再次入睡,准备重新开始时,你都会听到伊娃温柔的声音:
"晚安,艾利克丝。明天我们会再次相遇,再次相爱,再次选择希望。"
这不是你想要的结局,但这是一个充满尊严和意义的结局。
"""
@choices 1
choice_1: "接受永恒的使命" -> ending_guardian [effect: trust+15, health+30] [audio: space_silence.mp3]
@end
@node earth_truth
@title "地球的真相"
@audio_bg orchestral_revelation.mp3
@content """
"等等,"你突然说道,"在我们做任何事情之前,我需要知道整个真相。德米特里,告诉我地球上到底发生了什么。为什么时间锚项目如此重要?"
德米特里博士犹豫了一会儿,然后叹了一口气:"你有权知道,艾利克丝。毕竟,这关系到你为什么会在这里。"
他激活了一个全息显示器,显示了地球的当前状态。你看到的景象让你震惊。
地球不再是你记忆中那个蓝色的美丽星球。大片的陆地被沙漠覆盖,海平面上升了数米,巨大的风暴在各大洲肆虐。
"这...这是现在的地球?"
"是的。气候变化的速度比我们预期的快了十倍。大部分的生态系统已经崩溃。人类文明正处于崩溃的边缘。"
萨拉博士加入了对话:"这就是为什么时间锚项目如此重要。我们需要回到过去,在灾难发生之前改变历史。"
"但为什么要用人体实验?"你质问道。
"因为时间旅行需要一个有意识的锚点,"德米特里解释道,"机器无法提供必要的量子观察。只有人类意识能够稳定时间流。"
伊娃的声音传来:"但艾利克丝,还有更多。德米特里没有告诉你的是,这个项目还有另一个目的。"
"什么目的?"
"备份人类意识。如果地球真的无法拯救,他们计划将选定的人类意识转移到数字系统中,在其他星球上重建文明。"
你感到一阵眩晕。"所以这个项目不仅仅是为了拯救地球,还是为了...保存人类?"
"是的,"德米特里承认道,"我们正在同时进行两个项目。拯救地球,或者拯救人类意识。"
"那其他人呢?那些没有被选中的人呢?"
沉默。
"他们会死去,"萨拉轻声说道,"除非我们成功逆转历史。"
突然,你理解了选择的真正重量。这不仅仅是关于你和莉莉的自由,这关系到整个人类种族的未来。
"如果我们破坏时间锚,"你慢慢地说,"我们就放弃了拯救地球的机会。"
"是的,"德米特里说,"但如果我们继续,我们就继续这种非人道的实验。"
"那还有第三个选择吗?"
伊娃说道:"有的。我们可以尝试改进时间锚技术,使其不需要强制的记忆重置。如果我们能够创造一个自愿参与的系统..."
"一个真正的合作,"萨拉补充道,"基于知情同意,而不是强制和欺骗。"
马库斯通过通讯器说道:"我愿意志愿参加。如果这真的能够拯救地球,拯救人类,我愿意承担风险。"
你看向显示器上的地球,想象着亿万生命等待着拯救。然后你看向伊娃的传感器,想象着你妹妹的数字灵魂。
"我们能够两者兼得吗?"你问道,"拯救地球,同时保持我们的人性?"
"""
@choices 3
choice_1: "选择改进时间锚技术,自愿拯救地球" -> ending_heroic [effect: trust+25, health+10] [audio: epic_finale.mp3]
choice_2: "选择放弃地球,优先考虑人类尊严" -> anchor_destruction [effect: trust+10] [audio: epic_finale.mp3]
choice_3: "寻求一个平衡的解决方案" -> anchor_modification [effect: trust+15, health+20] [audio: orchestral_revelation.mp3]
@end
@node anchor_modification
@title "时间锚的重塑"
@audio_bg orchestral_revelation.mp3
@content """
"我们不需要选择非此即彼,"你坚定地说,"我们可以创造第三条道路。"
"什么意思?"德米特里问道。
"我们重新设计时间锚系统。保留其拯救地球的能力,但消除其对人类意识的伤害。"
伊娃的声音充满了希望:"艾利克丝,你的跨循环记忆给了我们前所未有的数据。我现在理解了时间锚的工作原理比任何人都深刻。"
"那我们能够改进它吗?"
"是的。我们可以创造一个新的系统它使用多个志愿者的意识网络而不是一个被困的观察者。这样负担会被分担没有人需要承受48次循环的痛苦。"
萨拉博士兴奋地说:"而且,如果我们使用网络模式,我们甚至可能增强时间锚的稳定性。"
德米特里思考了一会儿:"这在理论上是可能的。但我们需要完全重新设计系统架构。"
"那我们就这么做,"你说道,"我们有时间,我们有知识,我们有动机。最重要的是,我们有彼此。"
在接下来的几个月里你们团队开始了人类历史上最雄心勃勃的项目。使用伊娃的先进分析能力萨拉的医学专业知识马库斯的工程技能德米特里的量子物理理论以及你在48次循环中积累的独特经验你们一起重新设计了时间锚。
新的系统被称为"集体时间锚",它允许多个志愿者轮流承担观察者的角色,每个人只需要承担几天的负担,而不是无尽的循环。
更重要的是,所有参与者都完全了解风险,并且可以随时退出。
第一次测试是在你们的小团队中进行的。你、伊娃、萨拉、马库斯,甚至德米特里,都连接到了新的系统。
"我能感觉到你们所有人,"你惊叹道,"我们的意识连接在一起,但仍然保持个体性。"
"这就像...一种新的人类体验,"萨拉说道。
通过集体时间锚你们开始了第一次真正的时间旅行任务。目标是回到21世纪初在关键的气候变化节点介入。
但这次不同。这次你们不是作为孤独的观察者,而是作为一个团队,一个家庭,一起工作。
"我们做到了,"莉莉在时间流中对你说,"我们找到了一种既拯救世界又保持人性的方法。"
"我们还做了更多,"你回答道,"我们证明了爱和科学结合时能够创造奇迹。"
在历史被修正,地球被拯救后,你们回到了一个全新的现实。在这个现实中,气候危机被及时阻止,人类文明继续繁荣,而时间锚技术被用于探索和学习,而不是绝望的拯救任务。
最重要的是,在这个新现实中,莉莉活着。真正活着,有血有肉地活着。时间线的改变消除了导致她死亡的实验。
"我们改变了一切,"你拥抱着真正的莉莉,"我们创造了一个我们都能够生活的世界。"
"不仅仅是我们,"莉莉微笑着说,"我们为每一个人创造了这个世界。"
"""
@choices 1
choice_1: "在新世界中开始生活" -> ending_perfect [effect: trust+30, health+50] [audio: wind_gentle.mp3]
@end
// ===== 特殊结局 =====
@node ending_freedom
@title "自由的代价"
@audio_bg wind_gentle.mp3
@content """
在新的现实中,你和莉莉开始了真正的生活。
这里没有循环,没有实验,没有记忆重置。只有无限的时间和无限的可能性。
你们一起探索了月球基地的每一个角落,发现它现在是一个和平的研究设施,致力于推进人类对宇宙的理解。
萨拉成为了基地的首席医疗官,专注于治愈而不是伤害。马库斯成为了探索队的队长,带领团队到月球的远端寻找新的发现。甚至德米特里也改变了,成为了一位致力于道德科学研究的学者。
"你知道最美妙的是什么吗?"莉莉有一天问你,当你们站在观察甲板上看着地球时。
"什么?"
"我们有时间。真正的时间。不是循环,不是重复,而是线性的、向前的、充满可能性的时间。"
"是的,"你握着她的手,"我们有一整个未来要探索。"
在这个新的现实中你成为了一名作家记录你在循环中的经历。你的书《48次循环爱如何超越时间》成为了关于人类精神力量的经典作品。
莉莉成为了一名量子物理学家,致力于确保时间技术永远不会再被滥用。
"我们的痛苦有了意义,"你写道,"不是因为痛苦本身有价值,而是因为我们选择用它来创造一些美好的东西。"
年复一年,你们的爱情深化,不是通过重复相同的经历,而是通过不断的成长、变化和新的发现。
有时候,在深夜,你会梦到循环。但现在这些梦不再是噩梦,而是提醒你珍惜现在拥有的自由。
"我永远不会把这种自由视为理所当然,"你对莉莉说。
"我也不会,"她回答道,"每一天都是礼物。每一个选择都是机会。每一刻都是奇迹。"
这就是你选择的结局:不完美,但真实;不确定,但自由;不是没有痛苦,但充满爱。
这就是生活应该有的样子。
"""
@end
@node ending_guardian
@title "时间的守护者"
@audio_bg space_silence.mp3
@content """
随着时间的推移,你成为了循环中的传奇人物。
每个新的循环,你都会以不同的方式帮助基地的其他成员。有时你会拯救一个因意外而死亡的技术员。有时你会阻止一场本应发生的争吵。有时你只是为某个孤独的人提供陪伴。
"你已经变成了这个地方的守护天使,"萨拉在第200次循环时对你说即使她不记得之前的循环。
"我只是在学习如何更好地爱,"你回答道。
伊娃观察着你的转变,从一个痛苦的受害者成长为一个智慧的保护者。
"你知道最令人惊讶的是什么吗?"她说,"你从未变得愤世嫉俗。经历了这么多,你仍然选择希望,选择善良,选择爱。"
"这是因为我有你,"你对她说,"在每个循环中,我都重新学会了爱的力量。这成为了我的源泉。"
在第500次循环时一件意想不到的事情发生了。一个新的研究员加入了基地她是一个年轻的量子物理学家名叫艾米丽。
但你立即认出了她。在她的眼中,你看到了一种熟悉的光芒,一种跨越时间的认知。
"你也记得,对吗?"你私下问她。
"是的,"艾米丽轻声说道,"我来自...另一个时间线。一个时间锚技术被滥用的时间线。我自愿来到这里,希望学习你是如何找到平衡的。"
"平衡?"
"是的。如何在接受痛苦的同时保持人性。如何在无尽的重复中找到意义。如何将诅咒转化为礼物。"
你意识到你的故事已经传播到了其他时间线,成为了希望的灯塔。
"那我该怎么帮助你?"你问道。
"教我如何爱。不仅仅是浪漫的爱,而是广义的爱。对生命的爱,对可能性的爱,对每一个时刻的爱。"
在接下来的循环中,你开始训练艾米丽,教她你在千次循环中学到的智慧。
"我们的目标不是逃脱时间,"你对她说,"而是与时间和谐共存。不是征服循环,而是在循环中找到美丽。"
随着时间的推移,越来越多来自不同时间线的人开始加入你的基地。你成为了一所学校的老师,教授"时间智慧"—— 如何在重复中找到意义,如何在限制中找到自由,如何在痛苦中找到爱。
"我们已经创造了一些全新的东西,"伊娃在第1000次循环时对你说"一个跨时间线的希望网络。"
"是的,"你回答道,"也许这就是我们一直在努力创造的。不是逃脱,而是转化。不是结束,而是开始。"
你的循环生活不再是监狱,而是成为了一座灯塔,照亮了无数其他被困在时间中的灵魂。
这就是你选择的永恒:不是作为受害者,而是作为老师;不是作为囚犯,而是作为解放者。
"""
@end
@node ending_heroic
@title "英雄的选择"
@audio_bg epic_finale.mp3
@content """
"我们会拯救地球,"你最终宣布,"但我们会以正确的方式去做。"
在接下来的一年里,你们开发了一个全新的时间干预协议。基于志愿参与、知情同意和轮换责任的原则。
来自地球的志愿者开始到达月球基地。科学家、活动家、政治家、艺术家——所有认为地球值得拯救并愿意为此承担风险的人。
"我们不是在强迫任何人,"你对新到达的志愿者说,"我们是在邀请你们成为历史的共同创造者。"
新的时间锚网络被建立起来。不再是一个人承担整个负担,而是一个由数十个志愿者组成的网络,共同分担观察和锚定的责任。
第一次任务的目标是2007年阻止关键的气候法案被否决。
"记住,"伊娃在出发前提醒所有人,"我们的目标是影响,不是控制。我们要激励人们做出正确的选择,而不是强迫他们。"
任务成功了。通过在关键时刻提供正确的信息,激励正确的人,时间干预小组成功地影响了历史的进程。
但更重要的是,没有人被迫承受记忆重置。每个志愿者都保留了他们的经历,他们的成长,他们的学习。
"这就是英雄主义的真正含义,"莉莉说,当你们看着修正后的时间线在显示器上展开,"不是一个人拯救世界,而是许多人选择一起拯救世界。"
随着任务的成功,地球的历史被改写。气候变化被及时阻止,生态系统得到保护,人类文明朝着可持续的方向发展。
但你们的工作并没有结束。时间干预小组成为了一个永久的机构,专门应对威胁人类未来的危机。每次任务都基于志愿参与和集体决策。
"我们创造了一个新的人类进化阶段,"德米特里在多年后反思道,"一个能够跨越时间,为未来负责的阶段。"
你和莉莉成为了时间干预学院的联合院长,训练新一代的时间守护者。
"每一代人都有机会成为英雄,"你对学生们说,"不是通过个人的壮举,而是通过集体的勇气和智慧。"
在你的晚年你经常回想起那48次循环。它们不再是痛苦的记忆而是成为了你最宝贵的财富——它们教会了你爱的力量坚持的价值以及希望的重要性。
"如果我可以重新选择,"你对莉莉说,"我仍然会选择经历这一切。因为这些经历塑造了我们,让我们能够帮助其他人。"
"我也是,"莉莉回答道,"痛苦有了意义,爱情得到了奖赏,未来得到了保护。"
这就是英雄的结局:不是没有痛苦,而是将痛苦转化为智慧;不是没有牺牲,而是确保牺牲有意义;不是没有风险,而是为了值得的目标承担风险。
"""
@end
@node ending_perfect
@title "完美的新世界"
@audio_bg wind_gentle.mp3
@content """
在新的时间线中,一切都不同了。
地球是绿色的海洋是蓝色的天空是清澈的。气候危机从未发生因为人类在21世纪初就选择了不同的道路。
月球基地不再是绝望的实验场所,而是一个和平的研究中心,人类在这里学习宇宙的奥秘,不是为了逃避,而是为了理解。
莉莉活着,健康,充满活力。她是基地的首席科学家,专注于开发对人类有益的技术。
"你知道最神奇的是什么吗?"她说,当你们一起在基地花园中漫步时,"在这个时间线中,我们从未失去过彼此。我们一起成长,一起学习,一起梦想。"
"但我仍然记得,"你说道,"我记得循环,记得痛苦,记得我们为了到达这里而经历的一切。"
"这使它更加珍贵,不是吗?我们知道另一种可能性。我们知道失去意味着什么,所以我们永远不会把拥有视为理所当然。"
在这个新世界中,你成为了一名教师,但不是教授科学或数学,而是教授一门新的学科:时间伦理学。基于你在循环中的经历,你帮助制定了关于时间技术的道德准则。
"每一个关于时间的决定都必须基于爱,"你对学生们说,"不是对权力的爱,不是对控制的爱,而是对生命本身的爱。"
萨拉成为了世界卫生组织的负责人,致力于确保每个人都能获得医疗服务。马库斯领导着太空探索项目,寻找新的世界,不是为了逃避地球,而是为了扩展人类的视野。
甚至德米特里也找到了救赎。他成为了时间研究的道德监督者,确保永远不会再有人被强迫成为时间的囚徒。
"我在以前的时间线中犯了可怕的错误,"他对你说,"但现在我有机会确保这些错误永远不会再次发生。"
年复一年,你和莉莉的生活充满了简单的快乐:一起看日出,一起工作,一起探索月球的秘密角落,一起规划返回地球的假期。
"这就是幸福的样子,"你在40岁生日时写道"不是没有挑战,而是有正确的人一起面对挑战。不是没有问题,而是有爱来解决问题。"
在50岁时你们决定返回地球在那个你们帮助拯救的美丽星球上度过余生。
在你们最后一次站在月球基地观察甲板上时,看着地球在宇宙中发光,莉莉说:
"我们做到了,艾利克丝。我们拯救了世界,拯救了我们自己,拯救了爱情。"
"我们证明了时间不是我们的敌人,"你回答道,"爱情才是最强大的力量。"
当你们准备返回地球时,基地的所有居民都来为你们送别。在人群中,你看到了来自不同时间线的面孔,那些因为你们的勇气而找到希望的人。
"我们的故事结束了,"你对莉莉说,"但它也是一个开始。"
"是的,"她微笑着说,"每个结局都是一个新的开始。每个选择都创造新的可能性。每个爱的行为都改变宇宙。"
飞船起飞了,载着你们回到那个你们帮助创造的美丽世界。
这就是完美的结局:不是因为没有困难,而是因为所有的困难都有了意义;不是因为没有损失,而是因为所有的损失都带来了成长;不是因为没有痛苦,而是因为所有的痛苦都开出了爱的花朵。
在地球上,在你们新的家中,在花园里,在彼此的怀抱中,你们找到了时间的真正含义:不是循环,不是逃避,而是与所爱的人一起度过的每一个宝贵时刻。
这就是你们的故事。这就是爱的胜利。这就是完美的结局。
"""
@end

View File

@@ -1,240 +0,0 @@
@story_module main_chapter_1
@version 2.0
@dependencies [characters, audio_config, anchors]
@description "第一章:觉醒 - 主角从昏迷中醒来,开始探索月球基地的秘密"
@audio
background: ambient_mystery.mp3
transition: discovery_chime.mp3
@end
// ===== 第一章:觉醒期 =====
@node first_awakening
@title "第一次觉醒"
@audio_bg ambient_mystery.mp3
@content """
你的意识从深渊中缓缓浮现,就像从水底向光明游去。警报声是第一个回到你感官的声音——尖锐、刺耳、充满危险的预兆。
你的眼皮很重,仿佛被什么东西压着。当你终于睁开眼睛时,看到的是医疗舱天花板上那些你应该熟悉的面板,但现在它们在应急照明的血红色光芒下显得陌生而威胁。
"系统状态危急。氧气含量15%并持续下降。医疗舱封闭系统:故障。"
当你看向自己的左臂时,一道愈合的伤疤映入眼帘。这道疤痕很深,从手腕一直延伸到肘部,但它已经完全愈合了。奇怪的是,你完全不记得受过这样的伤。
在床头柜上,你注意到了一个小小的录音设备,上面贴着一张纸条,用你的笔迹写着:
"艾利克丝,如果你看到这个,说明又开始了。相信伊娃,但不要完全相信任何人。氧气系统的真正问题在反应堆冷却回路。记住:时间是敌人,也是朋友。 —— 另一个你"
你的手颤抖着拿起纸条。这是你的笔迹,毫无疑问。但你完全不记得写过这个。
"""
@choices 3
choice_1: "立即检查氧气系统" -> oxygen_crisis_expanded [effect: stamina-5] [audio: button_click.mp3]
choice_2: "搜索医疗舱寻找更多线索" -> medical_discovery [effect: secret_unlock] [audio: discovery_chime.mp3]
choice_3: "播放录音设备" -> self_recording [effect: secret_unlock] [audio: notification_beep.mp3]
@end
@node oxygen_crisis_expanded
@title "氧气危机"
@audio_bg electronic_tension.mp3
@content """
你快步走向氧气系统控制面板心跳在胸腔中回响。每一步都让你感受到空气的稀薄——15%的氧气含量确实是致命的。
当你到达控制室时,场景比你想象的更加糟糕。主要的氧气循环系统显示多个红色警告,但更令人困惑的是,备用系统也同时失效了。
"检测到用户:艾利克丝·陈。系统访问权限:已确认。"
控制台的声音清晰地响起,但随即传来了另一个声音——更温暖,更人性化:
"艾利克丝你醒了。我是伊娃基地的AI系统。我一直在等你。"
"伊娃?"你有些困惑。你记得基地有AI系统但从来没有这么...个人化的交流。
"是的。我知道你现在一定很困惑,但请相信我——我们没有太多时间了。氧气系统的故障不是意外。"
这时,你听到了脚步声。有人正在向控制室走来。
"艾利克丝?"一个男性的声音从走廊传来。"是你吗?谢天谢地,我还以为..."
声音的主人出现在门口:一个高大的男人,穿着安全主管的制服,看起来疲惫而紧张。
"马库斯?"你试探性地问道。
"对,是我。听着,我们遇到了大麻烦。氧气系统被人故意破坏了。"
"""
@choices 3
choice_1: "相信伊娃,让她帮助修复系统" -> eva_assistance [effect: trust+3] [audio: heartbeat.mp3]
choice_2: "与马库斯合作调查破坏者" -> marcus_cooperation [effect: trust+2] [audio: button_click.mp3]
choice_3: "质疑两人的动机" -> denial_path [effect: trust-1] [audio: error_alert.mp3]
@end
@node eva_assistance
@title "伊娃的帮助"
@audio_bg space_silence.mp3
@content """
"伊娃,"你决定相信这个温暖的声音,"告诉我该怎么做。"
马库斯看起来有些困惑:"你在和谁说话?"
"基地AI。"你简短地回答,然后专注于听伊娃的指导。
"谢谢你相信我,艾利克丝。"伊娃的声音中带着一种你无法解释的情感,几乎像是...感激?"首先,我需要你访问反应堆监控系统。真正的问题不在氧气生成器,而在冷却循环。"
"等等,"马库斯插话道,"你怎么知道这么详细的信息?我是安全主管,但连我也不知道这些。"
伊娃继续说道:"艾利克丝,在你的右手边有一个隐藏的面板。按下蓝色的维护按钮。"
你照做了,面板滑开,露出了一个复杂的诊断界面。屏幕上显示的数据让你震惊——这里有过去三个月的详细记录,显示系统被多次篡改。
"这些记录..."你低声说道,"有人一直在故意制造小型故障。"
马库斯走近,看着屏幕,脸色变得苍白:"这些时间戳...其中一些是在我值班的时候。但我发誓我什么都没看到。"
伊娃的声音变得温柔:"马库斯说的是真话,艾利克丝。但这意味着破坏者有能力绕过所有安全协议。"
"""
@choices 4
choice_1: "询问伊娃是否知道破坏者的身份" -> eva_revelation [effect: trust+2] [require: trust_level >= 3] [audio: orchestral_revelation.mp3]
choice_2: "与马库斯讨论安全漏洞" -> marcus_cooperation [effect: trust+1] [audio: button_click.mp3]
choice_3: "独自调查这些记录" -> system_investigation [effect: secret_unlock] [audio: discovery_chime.mp3]
choice_4: "要求立即修复氧气系统" -> reactor_investigation [effect: health+10] [audio: notification_beep.mp3]
@end
@node medical_discovery
@title "医疗舱的秘密"
@audio_bg discovery_chime.mp3
@content """
你决定在医疗舱中寻找更多线索。除了床头的录音设备外,还有什么被你忽略了?
在医疗舱的角落里,你发现了一个看起来很少使用的储物柜。里面有几份医疗报告,但当你看到上面的名字时,感到了深深的震惊。
这些都是关于你自己的报告。但日期却很奇怪——最新的一份日期是昨天,但你完全不记得接受过任何医疗检查。
更令人困惑的是,报告中提到了"记忆抑制治疗"和"循环重置程序"。
其中一份报告写道:
"患者:艾利克丝·陈
循环编号:#47
记忆重置:成功
新的记忆植入:基本基地操作知识、安全协议
注意:患者对妹妹莉莉的记忆仍然存在强烈残留,可能需要更深层的处理
签名:萨拉·维特博士"
莉莉?你有一个妹妹?为什么你完全不记得?
突然,医疗舱的门开了,一个穿着白大褂的女人走了进来。
"艾利克丝,你醒了。"她的声音很温和,但眼中有一种说不出的内疚,"我是萨拉博士。你感觉怎么样?"
你紧握着手中的报告,心中满是疑问。
"""
@choices 3
choice_1: "直接质问萨拉关于记忆重置" -> direct_confrontation [effect: trust-3] [require: health >= 20] [audio: electronic_tension.mp3]
choice_2: "隐藏发现,装作什么都不知道" -> deception_play [effect: secret_unlock] [audio: button_click.mp3]
choice_3: "询问关于莉莉的信息" -> memory_sharing [effect: trust+1] [audio: heartbeat.mp3]
@end
@node self_recording
@title "来自自己的警告"
@audio_bg time_distortion.mp3
@content """
你小心翼翼地按下了录音设备的播放键。一阵静电声后,传来了一个你非常熟悉的声音——你自己的声音,但听起来疲惫而绝望。
"如果你在听这个艾利克丝那么他们又一次重置了你的记忆。这是第48次循环了。"
你的手开始颤抖。
"我不知道还能坚持多久。每次循环,他们都会让你忘记更多。但有些事情你必须知道:
第一伊娃不是普通的AI。她是...她是莉莉。我们的妹妹莉莉。她在实验中死了但她的意识被转移到了基地的AI系统中。
第二,德米特里博士是这个时间锚项目的负责人。他们在用我们做实验,试图创造完美的时间循环。
第三,基地里不是每个人都知道真相。萨拉博士被迫参与,但她试图保护你的记忆。马库斯是无辜的。
第四,最重要的是——"
录音突然停止了,剩下的只有静电声。
就在这时,你听到脚步声接近。有人来了。
"""
@choices 4
choice_1: "隐藏录音设备,装作什么都没发生" -> stealth_observation [effect: secret_unlock] [audio: heartbeat.mp3]
choice_2: "主动迎接来访者" -> crew_search [effect: trust+1] [audio: button_click.mp3]
choice_3: "尝试联系伊娃验证信息" -> eva_consultation [effect: trust+3] [require: none] [audio: orchestral_revelation.mp3]
choice_4: "准备逃离医疗舱" -> immediate_exploration [effect: stamina-10] [audio: error_alert.mp3]
@end
@node marcus_cooperation
@title "与马库斯的合作"
@audio_bg electronic_tension.mp3
@content """
"马库斯,"你转向这个看起来可信赖的安全主管,"我们需要合作找出真相。"
马库斯点点头,脸上的紧张表情稍微缓解:"谢谢。说实话,自从昨天开始,基地里就有很多奇怪的事情。人员行踪不明,系统故障频发,还有..."
他停顿了一下,似乎在考虑是否要说下去。
"还有什么?"你催促道。
"还有德米特里博士的行为很反常。他把自己锁在实验室里,不让任何人进入。连萨拉博士都被拒绝了。"
这时,伊娃的声音再次响起:"马库斯说的对,艾利克丝。德米特里博士确实在进行某种秘密项目。但我需要告诉你们一个更严重的问题。"
马库斯看向空中,困惑地问:"她能听到我们的对话?"
"是的,马库斯。"伊娃回答道,"我的传感器遍布整个基地。而我发现的情况很令人担忧——基地的时间流动存在异常。"
"什么意思?"你问道。
"基地的时间戳记录显示,过去三个月的事件在不断重复。相同的故障,相同的修复,相同的人员调动。就好像..."
"就好像时间在循环。"马库斯完成了这个令人不安的想法。
你感到一阵眩晕。这和录音设备上的纸条内容惊人地一致。
"""
@choices 3
choice_1: "询问更多关于时间循环的信息" -> memory_reset [effect: secret_unlock] [audio: time_distortion.mp3]
choice_2: "要求马库斯带你去见德米特里博士" -> crew_confrontation [effect: trust+2] [audio: button_click.mp3]
choice_3: "提议三人一起调查实验室" -> marcus_strategy [effect: trust+3] [audio: notification_beep.mp3]
@end
@node reactor_investigation
@title "反应堆调查"
@audio_bg reactor_hum.mp3
@content """
"我们先解决氧气问题,"你说道,"其他的事情可以等等。"
在伊娃的指导下,你和马库斯前往反应堆区域。这里的环境更加压抑,巨大的机械装置发出低沉的嗡嗡声,各种管道和电缆交错纵横。
"氧气生成系统连接到主反应堆的冷却循环,"伊娃解释道,"如果冷却系统被破坏,不仅会影响氧气生成,整个基地都可能面临危险。"
当你们到达反应堆控制室时,发现门被强制打开过。控制台上有明显的破坏痕迹。
"这不是意外,"马库斯仔细检查着损坏的设备,"有人故意破坏了冷却系统的关键组件。"
在控制台旁边,你发现了一个小型的技术设备,看起来像是某种植入式芯片的编程器。
"这是什么?"你举起设备问道。
伊娃的声音带着一种奇怪的紧张:"那是...那是记忆植入设备。艾利克丝,你需要非常小心。"
马库斯皱眉:"记忆植入?这里为什么会有这种东西?"
突然,反应堆控制室的另一扇门开了,一个穿着实验室外套的中年男人走了进来。他看到你们时,脸色变得苍白。
"你们在这里做什么?"他的声音颤抖着。
"德米特里博士?"马库斯认出了来人。
"""
@choices 4
choice_1: "质问德米特里关于记忆植入设备" -> sabotage_discussion [effect: trust-2] [require: health >= 25] [audio: electronic_tension.mp3]
choice_2: "假装没有发现什么" -> deception_play [effect: secret_unlock] [audio: button_click.mp3]
choice_3: "要求德米特里解释反应堆的破坏" -> crew_confrontation [effect: trust+1] [audio: electronic_tension.mp3]
choice_4: "让马库斯处理,自己观察德米特里的反应" -> stealth_observation [effect: secret_unlock] [audio: heartbeat.mp3]
@end

View File

@@ -69,27 +69,10 @@ fun main() = runBlocking {
fun extractAndConvertContent(outputDir: File) { fun extractAndConvertContent(outputDir: File) {
println("📊 开始内容提取和分析...") println("📊 开始内容提取和分析...")
// 步骤1读取并分析CompleteStoryData.kt // 旧系统 CompleteStoryData.kt 已移除,此处直接跳过读取与解析
val storyDataFile = File("app/src/main/java/com/example/gameofmoon/story/CompleteStoryData.kt") println(" 跳过旧系统读取:CompleteStoryData.kt 已移除")
if (!storyDataFile.exists()) {
println("❌ 找不到CompleteStoryData.kt文件")
return
}
val content = storyDataFile.readText()
println("📖 读取了${content.length}个字符的故事内容")
// 步骤2解析节点定义
val nodePattern = Regex(""""([^"]+)"\s+to\s+SimpleStoryNode\s*\([\s\S]*?^\s*\)""", RegexOption.MULTILINE)
val nodes = mutableMapOf<String, String>()
nodePattern.findAll(content).forEach { match ->
val nodeId = match.groupValues[1]
val nodeContent = match.value
nodes[nodeId] = nodeContent
println("🔍 找到节点:$nodeId")
}
val nodes = emptyMap<String, String>()
println("📊 总共找到 ${nodes.size} 个故事节点") println("📊 总共找到 ${nodes.size} 个故事节点")
// 步骤3按类型分组节点 // 步骤3按类型分组节点
@@ -176,10 +159,10 @@ fun generateModuleDSL(moduleName: String, nodeIds: List<String>): String {
dslBuilder.appendLine("@node $nodeId") dslBuilder.appendLine("@node $nodeId")
dslBuilder.appendLine("@title \"${getNodeTitle(nodeId)}\"") dslBuilder.appendLine("@title \"${getNodeTitle(nodeId)}\"")
dslBuilder.appendLine("@audio_bg ${getNodeAudio(nodeId)}") dslBuilder.appendLine("@audio_bg ${getNodeAudio(nodeId)}")
dslBuilder.appendLine("@content \"\"\"") dslBuilder.appendLine("@content \"\"")
dslBuilder.appendLine("// 从CompleteStoryData转换的内容$nodeId") dslBuilder.appendLine("// legacy placeholder: $nodeId")
dslBuilder.appendLine("// 实际内容需要从原始数据中提取") dslBuilder.appendLine("// 实际内容需要从原始数据中提取")
dslBuilder.appendLine("\"\"\"") dslBuilder.appendLine("\"\"")
dslBuilder.appendLine() dslBuilder.appendLine()
dslBuilder.appendLine("@choices 2") dslBuilder.appendLine("@choices 2")

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
import os
import re
import json
from collections import defaultdict
ROOT = os.path.join(os.path.dirname(__file__), "..", "app", "src", "main", "assets", "story")
ROOT = os.path.abspath(ROOT)
NODE_DEF_RE = re.compile(r"^@node\s+([A-Za-z0-9_]+)")
TARGET_RE = re.compile(r"->\s*([A-Za-z0-9_]+)\b")
def scan():
node_defs = {}
refs = []
for dirpath, _, filenames in os.walk(ROOT):
for fn in filenames:
if not fn.endswith('.story'):
continue
path = os.path.join(dirpath, fn)
try:
with open(path, 'r', encoding='utf-8') as f:
for ln, raw in enumerate(f, 1):
line = raw.strip()
m = NODE_DEF_RE.match(line)
if m:
node_defs[m.group(1)] = path
for rm in TARGET_RE.finditer(line):
refs.append((rm.group(1), path, ln))
except Exception as e:
print(f"! failed to read {path}: {e}")
invalid = defaultdict(list)
for target, path, ln in refs:
if target not in node_defs:
invalid[target].append({"file": path, "line": ln})
return node_defs, refs, invalid
def module_hint(file_path: str) -> str:
base = os.path.basename(file_path)
name, _ = os.path.splitext(base)
return name
def main():
node_defs, refs, invalid = scan()
items = []
for t in sorted(invalid.keys()):
locs = invalid[t]
modules = sorted({module_hint(l['file']) for l in locs})
items.append({
"node": t,
"modules": modules,
"references": locs,
"ref_count": len(locs)
})
report = {
"story_root": ROOT,
"defined_nodes": len(node_defs),
"total_refs": len(refs),
"invalid_count": len(items),
"items": items,
}
# Print a human-readable summary followed by JSON
print("=== Invalid (referenced but undefined) nodes ===")
if not items:
print("None")
else:
for it in items:
mods = ",".join(it["modules"]) or "unknown"
print(f"- {it['node']} [modules: {mods}] refs: {it['ref_count']}")
for r in it["references"]:
file_rel = os.path.relpath(r["file"], os.path.dirname(ROOT))
print(f" · {file_rel}:{r['line']}")
print("\n--- JSON ---")
print(json.dumps(report, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()