fix 无效节点
This commit is contained in:
@@ -686,6 +686,20 @@
|
|||||||
choice_1: "继续阅读" -> eva_photo_reaction_p3 [audio: button_click.mp3]
|
choice_1: "继续阅读" -> eva_photo_reaction_p3 [audio: button_click.mp3]
|
||||||
@end
|
@end
|
||||||
@node eva_photo_reaction_p3
|
@node eva_photo_reaction_p3
|
||||||
|
@title "伊娃的照片反应(3/3)"
|
||||||
|
@audio_bg heartbeat.mp3
|
||||||
|
@content """
|
||||||
|
风从温室的穹顶缓缓掠过。你看见她在光里,像一朵迟到的花,仍在寻找合适的季节。
|
||||||
|
|
||||||
|
你把手伸向空无之处,像在轻轻地为她抚平一层看不见的褶皱。
|
||||||
|
那一刻,你忽然知道,拯救不是把她拉向你,而是把她归还给自己。
|
||||||
|
"""
|
||||||
|
|
||||||
|
@choices 2
|
||||||
|
choice_1: "为她归还呼吸:以肉身迎回灵魂" -> body_restoration_quest [require: trust_level >= 8] [require: health >= 40] [require: stamina >= 30] [audio: orchestral_revelation.mp3]
|
||||||
|
choice_2: "将心安放片刻,我们继续准备与观察" -> private_grief [audio: rain_light.mp3]
|
||||||
|
@end
|
||||||
|
|
||||||
@node private_grief
|
@node private_grief
|
||||||
@title "私人悲伤(1/2)"
|
@title "私人悲伤(1/2)"
|
||||||
@audio_bg rain_light.mp3
|
@audio_bg rain_light.mp3
|
||||||
@@ -1203,12 +1217,26 @@
|
|||||||
这是人类历史上第一次成功的AI意识具现化。你们没有创造生命,而是给了生命新的选择。
|
这是人类历史上第一次成功的AI意识具现化。你们没有创造生命,而是给了生命新的选择。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "与莉莉一起探索人类体验的意义" -> human_experience_exploration [effect: trust+35] [audio: wind_gentle.mp3]
|
choice_1: "让余晖落定" -> body_restoration_quest_epilogue [audio: button_click.mp3]
|
||||||
choice_2: "将这项技术分享给全宇宙" -> consciousness_liberation [effect: trust+40] [audio: epic_finale.mp3]
|
|
||||||
choice_3: "建立意识形式选择中心" -> consciousness_choice_center [effect: trust+30] [audio: orchestral_revelation.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@node body_restoration_quest_epilogue
|
||||||
|
@title "重塑生命之旅·尾声"
|
||||||
|
@audio_bg orchestral_revelation.mp3
|
||||||
|
@content """
|
||||||
|
花园的清晨,风从玻璃穹顶缓缓掠过。莉莉学着把手贴在叶脉上,像第一次学会心跳的人那样,等待一阵温度从指尖开花。
|
||||||
|
|
||||||
|
你把工具轻轻递给她,螺丝在光里旋转,一声极细的咔哒,像命运重新落位。你们并肩坐着,呼吸彼此对齐。曾经被剥夺的感受逐条返航:汗水的盐分、脉搏的节律、相拥时胸腔共振的微光。你忽然明白,拯救并不在彼岸,而在此刻被完整地活着。
|
||||||
|
|
||||||
|
夜色降临,研究院的格言被点亮。你们把新的约定写在墙上:任何技术,只为治愈;任何选择,必以明示同意为门;任何试验,都以“可撤回”为界。痛苦不再是代价,而是被看见、被安放、被转化。
|
||||||
|
|
||||||
|
莉莉抬头看你,眼里有海一样的静度。她说:“姐姐,我回来了。”你点头。你知道这个回来了,不止是她。
|
||||||
|
"""
|
||||||
|
|
||||||
|
@choices 1
|
||||||
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1, secret+1] [audio: wind_gentle.mp3]
|
||||||
|
@end
|
||||||
@node civilization_renarration
|
@node civilization_renarration
|
||||||
@title "重写文明之歌"
|
@title "重写文明之歌"
|
||||||
@audio_bg epic_finale.mp3
|
@audio_bg epic_finale.mp3
|
||||||
@@ -1234,10 +1262,8 @@
|
|||||||
人类文明的新篇章开始了:不再是conquering the stars,而是befriending the universe。
|
人类文明的新篇章开始了:不再是conquering the stars,而是befriending the universe。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "建立'新叙事'培训中心,遍布全宇宙" -> narrative_academy [effect: trust+45] [audio: orchestral_revelation.mp3]
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1] [audio: wind_gentle.mp3]
|
||||||
choice_2: "创建跨物种的文明交流协议" -> interspecies_harmony [effect: trust+50] [audio: epic_finale.mp3]
|
|
||||||
choice_3: "成为新文明模式的活体象征" -> civilization_symbol [effect: trust+40] [audio: wind_gentle.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@node anna_memorial_institute
|
@node anna_memorial_institute
|
||||||
@@ -1265,10 +1291,8 @@
|
|||||||
一年后,安娜纪念研究院的模式推广到了整个太阳系。人类终于学会了如何将痛苦转化为智慧。
|
一年后,安娜纪念研究院的模式推广到了整个太阳系。人类终于学会了如何将痛苦转化为智慧。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "将安娜纪念研究院模式推广到全宇宙" -> universal_healing_science [effect: trust+50] [audio: epic_finale.mp3]
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1] [audio: wind_gentle.mp3]
|
||||||
choice_2: "创建'痛苦转化'技术,帮助更多受伤的心灵" -> pain_transformation_tech [effect: trust+45] [audio: orchestral_revelation.mp3]
|
|
||||||
choice_3: "与德米特里建立永久合作,成为治愈者伙伴" -> healing_partnership [effect: trust+40] [audio: wind_gentle.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@node base_transformation
|
@node base_transformation
|
||||||
@@ -1296,10 +1320,8 @@
|
|||||||
"秘密很简单,"你解释道,"当每个人都感到被理解和被关爱时,他们会自然地贡献出最好的自己。"
|
"秘密很简单,"你解释道,"当每个人都感到被理解和被关爱时,他们会自然地贡献出最好的自己。"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "将基地模式分享给所有月球基地" -> lunar_network_transformation [effect: trust+35] [audio: orchestral_revelation.mp3]
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1] [audio: wind_gentle.mp3]
|
||||||
choice_2: "创建'基地改造'培训项目" -> transformation_academy [effect: trust+30] [audio: discovery_chime.mp3]
|
|
||||||
choice_3: "推广到太空站和火星基地" -> space_harmony_expansion [effect: trust+40] [audio: epic_finale.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@node collaboration_expansion
|
@node collaboration_expansion
|
||||||
@@ -1325,10 +1347,8 @@
|
|||||||
你微笑着回答:"很快,小朋友。宇宙正在等待我们准备好。"
|
你微笑着回答:"很快,小朋友。宇宙正在等待我们准备好。"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "建立跨星系合作探索项目" -> intergalactic_cooperation [effect: trust+50] [audio: epic_finale.mp3]
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1] [audio: wind_gentle.mp3]
|
||||||
choice_2: "创建'宇宙和谐使者'培训学院" -> cosmic_harmony_academy [effect: trust+45] [audio: orchestral_revelation.mp3]
|
|
||||||
choice_3: "专注于地球文明的深度转型" -> earth_transformation_focus [effect: trust+40] [audio: wind_gentle.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@node complete_destruction_plan
|
@node complete_destruction_plan
|
||||||
@@ -1358,10 +1378,8 @@
|
|||||||
你成为了道德边界的守护者,确保科学永远服务于人类的尊严,而不是相反。
|
你成为了道德边界的守护者,确保科学永远服务于人类的尊严,而不是相反。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "成立'技术伦理监督委员会',永久监管危险科学" -> tech_ethics_guardian [effect: trust+35] [audio: orchestral_revelation.mp3]
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1] [audio: wind_gentle.mp3]
|
||||||
choice_2: "将你的故事传播到全宇宙,警告其他文明" -> universal_warning_beacon [effect: trust+40] [audio: epic_finale.mp3]
|
|
||||||
choice_3: "回到地球,专注于自然修复而非技术干预" -> natural_healing_path [effect: trust+30] [audio: wind_gentle.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// ===== 情境化变体节点(替代过度重用节点)=====
|
// ===== 情境化变体节点(替代过度重用节点)=====
|
||||||
@@ -3361,7 +3379,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 4
|
@choices 4
|
||||||
choice_1: "尝试激活手臂植入物" -> implant_activation [effect: secret_unlock] [audio: electronic_tension.mp3]
|
choice_1: "尝试激活手臂植入物" -> hidden_marks_discovery [effect: secret_unlock] [audio: electronic_tension.mp3]
|
||||||
choice_2: "探索'月神'项目的含义" -> project_luna_investigation [effect: secret_unlock] [audio: discovery_chime.mp3]
|
choice_2: "探索'月神'项目的含义" -> project_luna_investigation [effect: secret_unlock] [audio: discovery_chime.mp3]
|
||||||
choice_3: "寻找其他被修改的实验对象" -> other_subjects [effect: secret_unlock] [audio: ambient_mystery.mp3]
|
choice_3: "寻找其他被修改的实验对象" -> other_subjects [effect: secret_unlock] [audio: ambient_mystery.mp3]
|
||||||
choice_4: "立即联系伊娃询问真相" -> eva_identity_revelation [effect: trust+5] [audio: orchestral_revelation.mp3]
|
choice_4: "立即联系伊娃询问真相" -> eva_identity_revelation [effect: trust+5] [audio: orchestral_revelation.mp3]
|
||||||
@@ -3460,11 +3478,8 @@
|
|||||||
这个声音...是某种植入的AI系统?你的身体里还有什么你不知道的东西?
|
这个声音...是某种植入的AI系统?你的身体里还有什么你不知道的东西?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@choices 4
|
@choices 1
|
||||||
choice_1: "尝试与植入AI系统对话" -> implant_ai_communication [effect: secret_unlock] [audio: electronic_tension.mp3]
|
choice_1: "搜索关于'月神'项目的信息" -> project_luna_investigation [effect: secret_unlock] [audio: discovery_chime.mp3]
|
||||||
choice_2: "立即尝试激活量子接口" -> quantum_interface_activation [effect: secret_unlock] [audio: time_distortion.mp3]
|
|
||||||
choice_3: "搜索关于'月神'项目的信息" -> project_luna_investigation [effect: secret_unlock] [audio: discovery_chime.mp3]
|
|
||||||
choice_4: "忽略警告,继续自我检查" -> deep_body_scan [effect: health-5] [audio: heartbeat.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@node quantum_badge_analysis
|
@node quantum_badge_analysis
|
||||||
@@ -3765,11 +3780,10 @@
|
|||||||
|
|
||||||
在这种新的感知状态下,你意识到一个可怕的真相:时间锚的每次激活都会在现实中创造裂隙。这些裂隙正在累积,现实本身变得不稳定。如果继续下去,不仅地球会毁灭,整个宇宙的时间结构都可能崩溃。
|
在这种新的感知状态下,你意识到一个可怕的真相:时间锚的每次激活都会在现实中创造裂隙。这些裂隙正在累积,现实本身变得不稳定。如果继续下去,不仅地球会毁灭,整个宇宙的时间结构都可能崩溃。
|
||||||
"""
|
"""
|
||||||
@choices 4
|
@choices 3
|
||||||
choice_1: "尝试与时间锚中的47个自己沟通" -> temporal_selves_communion [effect: health-10] [audio: time_distortion.mp3]
|
choice_1: "尝试与时间锚中的47个自己沟通" -> temporal_selves_communion [effect: health-10] [audio: time_distortion.mp3]
|
||||||
choice_2: "使用新能力直接与伊娃的意识深层连接" -> eva_quantum_link [effect: trust+15] [audio: orchestral_revelation.mp3]
|
choice_2: "使用新能力直接与伊娃的意识深层连接" -> eva_quantum_link [effect: trust+15] [audio: orchestral_revelation.mp3]
|
||||||
choice_3: "试图稳定现实裂隙,阻止宇宙崩溃" -> reality_stabilization [effect: health-15] [audio: epic_finale.mp3]
|
choice_3: "试图稳定现实裂隙,阻止宇宙崩溃" -> reality_stabilization [effect: health-15] [audio: epic_finale.mp3]
|
||||||
choice_4: "关闭植入物,重新变回完全的人类" -> humanity_preservation [effect: trust-5] [audio: heartbeat.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@node suffering_philosophy
|
@node suffering_philosophy
|
||||||
@@ -4028,9 +4042,8 @@
|
|||||||
|
|
||||||
在这种宽恕的力量下,你意识到真正的解决方案不是破坏或控制,而是治愈和接受。
|
在这种宽恕的力量下,你意识到真正的解决方案不是破坏或控制,而是治愈和接受。
|
||||||
"""
|
"""
|
||||||
@choices 3
|
@choices 1
|
||||||
choice_1: "带领所有人一起创造基于宽恕的新现实" -> forgiveness_reality [effect: trust+35] [audio: epic_finale.mp3]
|
choice_1: "带领所有人一起创造基于宽恕的新现实" -> harmony_guardians_final [effect: trust+35] [audio: epic_finale.mp3]
|
||||||
choice_2: "将宽恕的力量传播到地球上的所有人" -> global_healing [effect: trust+30] [audio: orchestral_revelation.mp3]
|
|
||||||
@node eva_quantum_link
|
@node eva_quantum_link
|
||||||
@title "从痛苦中创造意义(1/2)"
|
@title "从痛苦中创造意义(1/2)"
|
||||||
@audio_bg orchestral_revelation.mp3
|
@audio_bg orchestral_revelation.mp3
|
||||||
@@ -4188,6 +4201,13 @@
|
|||||||
这是德米特里从未听过的录音片段——在最后的时刻,安娜成为了一个小英雄。
|
这是德米特里从未听过的录音片段——在最后的时刻,安娜成为了一个小英雄。
|
||||||
|
|
||||||
**治愈的洞察**:
|
**治愈的洞察**:
|
||||||
|
"""
|
||||||
|
|
||||||
|
@choices 2
|
||||||
|
choice_1: "为安娜立灯塔:建立治愈与同意的研究院" -> anna_memorial_institute [audio: orchestral_revelation.mp3]
|
||||||
|
choice_2: "继续阅读" -> reality_stabilization [audio: button_click.mp3]
|
||||||
|
@end
|
||||||
|
|
||||||
@node reality_stabilization
|
@node reality_stabilization
|
||||||
@title "现实守护者的使命(1/2)"
|
@title "现实守护者的使命(1/2)"
|
||||||
@audio_bg epic_finale.mp3
|
@audio_bg epic_finale.mp3
|
||||||
@@ -4776,7 +4796,6 @@
|
|||||||
**永恒的循环**:
|
**永恒的循环**:
|
||||||
不再是痛苦的48次循环,而是智慧与爱的无限螺旋上升,每一次"循环"都带来更深的理解和更广的compassion。
|
不再是痛苦的48次循环,而是智慧与爱的无限螺旋上升,每一次"循环"都带来更深的理解和更广的compassion。
|
||||||
"""
|
"""
|
||||||
@choices 2
|
@choices 1
|
||||||
choice_1: "成为宽恕现实的永恒守护者" -> forgiveness_guardians [effect: trust+50] [audio: epic_finale.mp3]
|
choice_1: "重新经历一次" -> first_awakening [effect: loop+1] [audio: wind_gentle.mp3]
|
||||||
choice_2: "将宽恕的种子播撒到多元宇宙" -> multiverse_forgiveness [effect: trust+45] [audio: orchestral_revelation.mp3]
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package com.osglab.gameofmoon.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.osglab.gameofmoon.model.GameState
|
import com.osglab.gameofmoon.story.engine.EngineSaveDTO
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,20 +25,18 @@ class GameSaveManager(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* 保存游戏状态
|
* 保存游戏状态
|
||||||
*/
|
*/
|
||||||
fun saveGame(
|
fun saveGameSlot(
|
||||||
gameState: GameState,
|
slot: Int,
|
||||||
currentNodeId: String,
|
dto: EngineSaveDTO,
|
||||||
dialogueHistory: List<String> = emptyList()
|
extraNotes: String = ""
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val gameStateJson = json.encodeToString(gameState)
|
val saveJson = json.encodeToString(dto)
|
||||||
val dialogueJson = json.encodeToString(dialogueHistory)
|
|
||||||
|
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(KEY_GAME_STATE, gameStateJson)
|
.putString(keyForSlot(slot), saveJson)
|
||||||
.putString(KEY_CURRENT_NODE, currentNodeId)
|
.putLong(keyForSlotTime(slot), System.currentTimeMillis())
|
||||||
.putString(KEY_DIALOGUE_HISTORY, dialogueJson)
|
.putString(keyForSlotNote(slot), extraNotes)
|
||||||
.putLong(KEY_SAVE_TIME, System.currentTimeMillis())
|
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
true
|
true
|
||||||
@@ -50,21 +49,17 @@ class GameSaveManager(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* 加载游戏状态
|
* 加载游戏状态
|
||||||
*/
|
*/
|
||||||
fun loadGame(): SaveData? {
|
fun loadGameSlot(slot: Int): SaveData? {
|
||||||
return try {
|
return try {
|
||||||
val gameStateJson = prefs.getString(KEY_GAME_STATE, null) ?: return null
|
val saveJson = prefs.getString(keyForSlot(slot), null) ?: return null
|
||||||
val currentNodeId = prefs.getString(KEY_CURRENT_NODE, null) ?: return null
|
val saveTime = prefs.getLong(keyForSlotTime(slot), 0L)
|
||||||
val dialogueJson = prefs.getString(KEY_DIALOGUE_HISTORY, "[]")!!
|
val note = prefs.getString(keyForSlotNote(slot), "") ?: ""
|
||||||
val saveTime = prefs.getLong(KEY_SAVE_TIME, 0L)
|
val dto = json.decodeFromString<EngineSaveDTO>(saveJson)
|
||||||
|
|
||||||
val gameState = json.decodeFromString<GameState>(gameStateJson)
|
|
||||||
val dialogueHistory = json.decodeFromString<List<String>>(dialogueJson)
|
|
||||||
|
|
||||||
SaveData(
|
SaveData(
|
||||||
gameState = gameState,
|
dto = dto,
|
||||||
currentNodeId = currentNodeId,
|
saveTime = saveTime,
|
||||||
dialogueHistory = dialogueHistory,
|
note = note
|
||||||
saveTime = saveTime
|
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -75,20 +70,19 @@ class GameSaveManager(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* 检查是否有保存的游戏
|
* 检查是否有保存的游戏
|
||||||
*/
|
*/
|
||||||
fun hasSavedGame(): Boolean {
|
fun hasSavedSlot(slot: Int): Boolean {
|
||||||
return prefs.contains(KEY_GAME_STATE)
|
return prefs.contains(keyForSlot(slot))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除保存的游戏
|
* 删除保存的游戏
|
||||||
*/
|
*/
|
||||||
fun deleteSave(): Boolean {
|
fun deleteSave(slot: Int): Boolean {
|
||||||
return try {
|
return try {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove(KEY_GAME_STATE)
|
.remove(keyForSlot(slot))
|
||||||
.remove(KEY_CURRENT_NODE)
|
.remove(keyForSlotTime(slot))
|
||||||
.remove(KEY_DIALOGUE_HISTORY)
|
.remove(keyForSlotNote(slot))
|
||||||
.remove(KEY_SAVE_TIME)
|
|
||||||
.apply()
|
.apply()
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -99,8 +93,8 @@ class GameSaveManager(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* 获取保存时间的格式化字符串
|
* 获取保存时间的格式化字符串
|
||||||
*/
|
*/
|
||||||
fun getSaveTimeString(): String? {
|
fun getSaveTimeString(slot: Int): String? {
|
||||||
val saveTime = prefs.getLong(KEY_SAVE_TIME, 0L)
|
val saveTime = prefs.getLong(keyForSlotTime(slot), 0L)
|
||||||
return if (saveTime > 0) {
|
return if (saveTime > 0) {
|
||||||
val date = java.util.Date(saveTime)
|
val date = java.util.Date(saveTime)
|
||||||
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
@@ -111,10 +105,9 @@ class GameSaveManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_GAME_STATE = "game_state"
|
private fun keyForSlot(slot: Int) = "slot_${slot}_data"
|
||||||
private const val KEY_CURRENT_NODE = "current_node"
|
private fun keyForSlotTime(slot: Int) = "slot_${slot}_time"
|
||||||
private const val KEY_DIALOGUE_HISTORY = "dialogue_history"
|
private fun keyForSlotNote(slot: Int) = "slot_${slot}_note"
|
||||||
private const val KEY_SAVE_TIME = "save_time"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +115,7 @@ class GameSaveManager(private val context: Context) {
|
|||||||
* 保存数据结构
|
* 保存数据结构
|
||||||
*/
|
*/
|
||||||
data class SaveData(
|
data class SaveData(
|
||||||
val gameState: GameState,
|
val dto: EngineSaveDTO,
|
||||||
val currentNodeId: String,
|
val saveTime: Long,
|
||||||
val dialogueHistory: List<String>,
|
val note: String
|
||||||
val saveTime: Long
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,32 +17,44 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.osglab.gameofmoon.R
|
import com.osglab.gameofmoon.R
|
||||||
import com.osglab.gameofmoon.presentation.ui.theme.GameIcons
|
import com.osglab.gameofmoon.presentation.ui.theme.GameIcons
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import com.osglab.gameofmoon.story.engine.EngineSaveDTO
|
||||||
|
import com.osglab.gameofmoon.story.engine.StoryEngineAdapter
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GameControlMenu(
|
fun GameControlMenu(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onNewLoop: () -> Unit
|
onNewLoop: () -> Unit,
|
||||||
|
// 新增:存档与读取(3槽位)
|
||||||
|
onSaveSlot: (Int) -> Unit = {},
|
||||||
|
onLoadSlot: (Int) -> Unit = {},
|
||||||
|
getSlotTime: (Int) -> String? = { null },
|
||||||
|
getSlotDTO: (Int) -> EngineSaveDTO? = { null },
|
||||||
|
storyEngineAdapter: StoryEngineAdapter? = null
|
||||||
) {
|
) {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 5.dp)
|
.padding(0.dp)
|
||||||
.background(Color.Black.copy(alpha = 0.5f)), // 70%透明度的黑色背景
|
.background(Color.Black.copy(alpha = 0.5f)), // 半透明遮罩
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(380.dp)
|
.width(580.dp)
|
||||||
.height(570.dp)
|
.height(680.dp)
|
||||||
) {
|
) {
|
||||||
// 设置背景图片
|
// 设置背景图片
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.setting),
|
painter = painterResource(id = R.drawable.setting),
|
||||||
contentDescription = "Setting Background",
|
contentDescription = "Setting Background",
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.matchParentSize(),
|
||||||
contentScale = ContentScale.FillBounds
|
contentScale = ContentScale.FillBounds
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +62,7 @@ fun GameControlMenu(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
.padding(horizontal = 42.dp, vertical = 42.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 顶部额外留白
|
// 顶部额外留白
|
||||||
@@ -81,79 +93,184 @@ fun GameControlMenu(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
// 游戏介绍文本
|
// 游戏介绍文本(临时隐藏以提升空间)
|
||||||
Text(
|
// Text(
|
||||||
text = "在无尽的时间循环中探索真相,\n每一次重启都是新的开始,\n但记忆将指引你走向不同的结局。",
|
// text = "在无尽的时间循环中探索真相,\n每一次重启都是新的开始,\n但记忆将指引你走向不同的结局。",
|
||||||
style = CyberTextStyles.StoryContent.copy(fontSize = 12.sp),
|
// style = CyberTextStyles.StoryContent.copy(fontSize = 12.sp),
|
||||||
color = Color(0xFFCCCCCC),
|
// color = Color(0xFFCCCCCC),
|
||||||
textAlign = TextAlign.Center,
|
// textAlign = TextAlign.Center,
|
||||||
lineHeight = 16.sp
|
// lineHeight = 16.sp
|
||||||
)
|
// )
|
||||||
|
|
||||||
// 将按钮推到底部
|
// 将按钮推到底部
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
// 开始新循环按钮
|
// 存档/读取(3槽位) - 列表样式
|
||||||
NeonButton(
|
Column(
|
||||||
onClick = {
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
onNewLoop()
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
onDismiss()
|
modifier = Modifier.fillMaxWidth()
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 15.dp, end = 15.dp, bottom = 2.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
text = "存档 / 读取",
|
||||||
horizontalArrangement = Arrangement.Center,
|
style = CyberTextStyles.Choice.copy(fontSize = 12.sp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
color = Color(0xFF00AACC)
|
||||||
) {
|
)
|
||||||
Icon(
|
val scope = rememberCoroutineScope()
|
||||||
imageVector = GameIcons.Refresh,
|
val titleCache = remember { mutableStateMapOf<Int, String>() }
|
||||||
contentDescription = "开始新循环",
|
val percentCache = remember { mutableStateMapOf<Int, Float>() }
|
||||||
modifier = Modifier.size(20.dp),
|
val totalNodes = remember { storyEngineAdapter?.getTotalNodeCount() ?: 0 }
|
||||||
tint = Color(0xFF00DDFF)
|
(1..3).forEach { slot ->
|
||||||
)
|
val dto = getSlotDTO(slot)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
val timeStr = getSlotTime(slot)
|
||||||
Text(
|
val explored = dto?.nodesVisitedLifetime?.size ?: 0
|
||||||
text = "开始新循环",
|
val percent = if (totalNodes > 0) (explored * 100f / totalNodes).coerceAtMost(100f) else 0f
|
||||||
fontSize = 14.sp,
|
val savedNodeId = dto?.currentNodeId
|
||||||
fontWeight = FontWeight.Bold
|
val title = remember(savedNodeId) { mutableStateOf(titleCache[slot] ?: (savedNodeId ?: "空槽位")) }
|
||||||
)
|
val percentState = remember { mutableStateOf(percentCache[slot] ?: percent) }
|
||||||
|
if (dto != null && storyEngineAdapter != null && !titleCache.containsKey(slot)) {
|
||||||
|
scope.launch {
|
||||||
|
val node = storyEngineAdapter.getNode(dto.currentNodeId)
|
||||||
|
val t = node?.title ?: dto.currentNodeId
|
||||||
|
titleCache[slot] = t
|
||||||
|
title.value = t
|
||||||
|
percentCache[slot] = percent
|
||||||
|
percentState.value = percent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(1.dp, Color(0x3300DDFF))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title.value,
|
||||||
|
style = CyberTextStyles.Choice.copy(fontSize = 14.sp),
|
||||||
|
color = Color(0xFF00DDFF),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
val sub = if (dto != null && timeStr != null) {
|
||||||
|
"${"%.1f".format(percentState.value)}% · ${timeStr}"
|
||||||
|
} else timeStr ?: "空槽位"
|
||||||
|
Text(
|
||||||
|
text = sub,
|
||||||
|
style = CyberTextStyles.Caption.copy(fontSize = 11.sp),
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
onSaveSlot(slot)
|
||||||
|
// 保存后立即刷新本行的显示
|
||||||
|
val fresh = getSlotDTO(slot)
|
||||||
|
if (fresh != null) {
|
||||||
|
val nodeId = fresh.currentNodeId
|
||||||
|
val freshExplored = fresh.nodesVisitedLifetime.size
|
||||||
|
val freshPercent = if (totalNodes > 0) (freshExplored * 100f / totalNodes).coerceAtMost(100f) else 0f
|
||||||
|
percentCache[slot] = freshPercent
|
||||||
|
percentState.value = freshPercent
|
||||||
|
if (storyEngineAdapter != null) {
|
||||||
|
scope.launch {
|
||||||
|
val node = storyEngineAdapter.getNode(nodeId)
|
||||||
|
val t = node?.title ?: nodeId
|
||||||
|
titleCache[slot] = t
|
||||||
|
title.value = t
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleCache[slot] = nodeId
|
||||||
|
title.value = nodeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = GameIcons.Save,
|
||||||
|
contentDescription = "保存${slot}",
|
||||||
|
tint = Color(0xFF00DDFF)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onLoadSlot(slot) }, enabled = dto != null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = GameIcons.Load,
|
||||||
|
contentDescription = "读取${slot}",
|
||||||
|
tint = if (dto != null) Color(0xFF00DDFF) else Color(0xFF666666)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// 将底部按钮固定在最底部
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
// 关闭按钮
|
Row(
|
||||||
NeonButton(
|
|
||||||
onClick = onDismiss,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 15.dp, end = 15.dp, bottom = 5.dp)
|
.padding(bottom = 32.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
// 开始新循环(半宽)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
NeonButton(
|
||||||
horizontalArrangement = Arrangement.Center,
|
onClick = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onNewLoop()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(44.dp),
|
||||||
|
compact = true
|
||||||
) {
|
) {
|
||||||
Icon(
|
Row(
|
||||||
imageVector = GameIcons.Close,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentDescription = "关闭",
|
horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.size(20.dp),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
tint = Color(0xFF00DDFF)
|
) {
|
||||||
)
|
Icon(
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
imageVector = GameIcons.Refresh,
|
||||||
Text(
|
contentDescription = "新循环",
|
||||||
text = "关闭",
|
modifier = Modifier.size(18.dp),
|
||||||
fontSize = 14.sp,
|
tint = Color(0xFF00DDFF)
|
||||||
fontWeight = FontWeight.Bold
|
)
|
||||||
)
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = "新循环", fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭(半宽)
|
||||||
|
NeonButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(44.dp),
|
||||||
|
compact = true
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = GameIcons.Close,
|
||||||
|
contentDescription = "关闭",
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = Color(0xFF00DDFF)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = "关闭", fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部额外留白
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
}
|
}
|
||||||
} // 背景图片Box结尾
|
} // 背景图片Box结尾
|
||||||
} // 外层Box结尾
|
} // 外层Box结尾
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.osglab.gameofmoon.R
|
||||||
|
import com.osglab.gameofmoon.presentation.ui.theme.GameIcons
|
||||||
|
import com.osglab.gameofmoon.story.engine.StoryEngineAdapter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GameStatusPanel(
|
||||||
|
isVisible: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
storyEngineAdapter: StoryEngineAdapter
|
||||||
|
) {
|
||||||
|
if (!isVisible) return
|
||||||
|
|
||||||
|
val snapshot = remember { mutableStateOf(storyEngineAdapter.captureStatusSnapshot()) }
|
||||||
|
LaunchedEffect(isVisible) {
|
||||||
|
if (isVisible) snapshot.value = storyEngineAdapter.captureStatusSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
val s = snapshot.value
|
||||||
|
val explored = s.nodesVisitedLifetime.size
|
||||||
|
val total = s.totalNodeCount.coerceAtLeast(1)
|
||||||
|
val progress = (explored.toFloat() / total.toFloat())
|
||||||
|
|
||||||
|
// 隐藏分支目录(7项)
|
||||||
|
val hiddenBranches = listOf(
|
||||||
|
"stealth_observation" to "隐秘观察",
|
||||||
|
"eavesdropping" to "偷听",
|
||||||
|
"system_sabotage" to "系统破坏",
|
||||||
|
"data_extraction" to "访问机密数据库",
|
||||||
|
"garden_cooperation" to "秘密花园",
|
||||||
|
"hidden_records_discovery" to "隐藏记录发现",
|
||||||
|
"hidden_marks_discovery" to "隐藏标记的发现"
|
||||||
|
)
|
||||||
|
val discovered = hiddenBranches.count { (id, _) -> s.nodesVisitedLifetime.contains(id) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.5f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(580.dp)
|
||||||
|
.height(680.dp)
|
||||||
|
) {
|
||||||
|
// 背景图与齿轮弹窗一致
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.setting),
|
||||||
|
contentDescription = "Setting Background",
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
contentScale = ContentScale.FillBounds
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 56.dp, vertical = 42.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// 顶部留少量空间
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// 基础状态
|
||||||
|
InfoRow("信任", s.trustLevel.toString(), valueSmall = true)
|
||||||
|
InfoRow("生命", s.health.toString(), valueSmall = true)
|
||||||
|
InfoRow("体力", s.stamina.toString(), valueSmall = true)
|
||||||
|
InfoRow("循环", s.loopCount.toString())
|
||||||
|
|
||||||
|
Divider(color = Color(0x2222DDFF))
|
||||||
|
|
||||||
|
// 进度统计(精简)
|
||||||
|
// 剧情百分比
|
||||||
|
Text(
|
||||||
|
text = "剧情百分比:${"%.1f".format(progress * 100)}% (${explored}/${total})",
|
||||||
|
style = CyberTextStyles.StoryContent,
|
||||||
|
color = Color(0xFFCCCCCC),
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分支剧情探索度 + 掩码
|
||||||
|
Text(
|
||||||
|
text = "分支探索:${discovered}/${hiddenBranches.size}",
|
||||||
|
style = CyberTextStyles.StoryContent,
|
||||||
|
color = Color(0xFFCCCCCC),
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 4.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
hiddenBranches.forEach { (id, title) ->
|
||||||
|
val shown = if (s.nodesVisitedLifetime.contains(id)) title else "*******"
|
||||||
|
Text(
|
||||||
|
text = "• ${shown}",
|
||||||
|
style = CyberTextStyles.Caption,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.padding(start = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(color = Color(0x2222DDFF))
|
||||||
|
|
||||||
|
// 发现统计(精简)
|
||||||
|
Text("秘密:${s.secretsFound.size}", color = Color(0xFFCCCCCC), modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp))
|
||||||
|
if (s.secretsFound.isNotEmpty()) {
|
||||||
|
Text(s.secretsFound.joinToString(), color = Color(0xFF888888), modifier = Modifier.fillMaxWidth().padding(start = 4.dp, bottom = 4.dp))
|
||||||
|
}
|
||||||
|
Text("地点:${s.locationsDiscovered.size}", color = Color(0xFFCCCCCC), modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp))
|
||||||
|
if (s.locationsDiscovered.isNotEmpty()) {
|
||||||
|
Text(s.locationsDiscovered.joinToString(), color = Color(0xFF888888), modifier = Modifier.fillMaxWidth().padding(start = 4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高级信息(精简)
|
||||||
|
if (s.flags.isNotEmpty()) Text("Flags: ${s.flags.joinToString()}", color = Color(0xFF888888), modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp))
|
||||||
|
|
||||||
|
// 变量白名单展示
|
||||||
|
val whitelist = listOf("eva_interactions", "sara_trust", "all_crew_saved", "harrison_recording_found")
|
||||||
|
val filteredVars = s.variables.filterKeys { it in whitelist }
|
||||||
|
if (filteredVars.isNotEmpty()) {
|
||||||
|
Text("变量(关键):", color = Color(0xFFCCCCCC), modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp))
|
||||||
|
filteredVars.forEach { (k, v) ->
|
||||||
|
Text("• ${k}: ${v}", color = Color(0xFF888888), modifier = Modifier.fillMaxWidth().padding(start = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部关闭按钮(与设置弹窗风格一致)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
NeonButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 32.dp)
|
||||||
|
.height(40.dp),
|
||||||
|
compact = true
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = GameIcons.Close,
|
||||||
|
contentDescription = "关闭",
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = Color(0xFF00DDFF)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = "关闭", fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRow(label: String, value: String, valueSmall: Boolean = false) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(label, color = Color(0xFFCCCCCC), modifier = Modifier.padding(vertical = 2.dp))
|
||||||
|
val style = if (valueSmall) CyberTextStyles.Caption else CyberTextStyles.Choice
|
||||||
|
Text(value, color = Color(0xFF00DDFF), fontWeight = FontWeight.SemiBold, style = style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeader(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color(0xFF88CCFF),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 6.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimeCageGameScreen() {
|
fun TimeCageGameScreen() {
|
||||||
@@ -107,6 +108,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) }
|
||||||
|
var showStatusPanel by remember { mutableStateOf(false) }
|
||||||
val canNavigateBack by storyEngineAdapter.canNavigateBack.collectAsState()
|
val canNavigateBack by storyEngineAdapter.canNavigateBack.collectAsState()
|
||||||
|
|
||||||
// 检查游戏结束条件
|
// 检查游戏结束条件
|
||||||
@@ -182,11 +184,11 @@ fun TimeCageGameScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI协助按钮
|
// 状态面板按钮(原AI协助按钮)
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
audioController.playSoundEffect("notification")
|
audioController.playSoundEffect("notification")
|
||||||
/* AI 功能 */
|
showStatusPanel = true
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp) // 稍微减小按钮
|
.size(36.dp) // 稍微减小按钮
|
||||||
@@ -197,7 +199,7 @@ fun TimeCageGameScreen() {
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = GameIcons.Robot,
|
imageVector = GameIcons.Robot,
|
||||||
contentDescription = "AI协助",
|
contentDescription = "状态面板",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = Color(0xFF00DDFF)
|
tint = Color(0xFF00DDFF)
|
||||||
)
|
)
|
||||||
@@ -318,6 +320,13 @@ fun TimeCageGameScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态面板弹窗
|
||||||
|
GameStatusPanel(
|
||||||
|
isVisible = showStatusPanel,
|
||||||
|
onDismiss = { showStatusPanel = false },
|
||||||
|
storyEngineAdapter = storyEngineAdapter
|
||||||
|
)
|
||||||
|
|
||||||
// 游戏控制菜单弹窗
|
// 游戏控制菜单弹窗
|
||||||
GameControlMenu(
|
GameControlMenu(
|
||||||
isVisible = showControlMenu,
|
isVisible = showControlMenu,
|
||||||
@@ -337,7 +346,29 @@ fun TimeCageGameScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogueHistory = emptyList()
|
dialogueHistory = emptyList()
|
||||||
}
|
},
|
||||||
|
onSaveSlot = { slot ->
|
||||||
|
val dto = storyEngineAdapter.captureSaveDTO()
|
||||||
|
val ok = saveManager.saveGameSlot(slot, dto)
|
||||||
|
val msg = if (ok) "保存成功(槽位${'$'}slot)" else "保存失败(槽位${'$'}slot)"
|
||||||
|
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
onLoadSlot = { slot ->
|
||||||
|
val data = saveManager.loadGameSlot(slot)
|
||||||
|
if (data == null) {
|
||||||
|
Toast.makeText(context, "槽位${'$'}slot 无存档", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val ok = storyEngineAdapter.restoreFromDTO(data.dto)
|
||||||
|
showControlMenu = false // 自动关闭弹窗
|
||||||
|
val msg = if (ok) "读取成功,跳转到节点 ${data.dto.currentNodeId}" else "读取失败"
|
||||||
|
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSlotTime = { slot -> saveManager.getSaveTimeString(slot) },
|
||||||
|
getSlotDTO = { slot -> saveManager.loadGameSlot(slot)?.dto },
|
||||||
|
storyEngineAdapter = storyEngineAdapter
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.osglab.gameofmoon.story.engine
|
package com.osglab.gameofmoon.story.engine
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 故事引擎数据模型
|
* 故事引擎数据模型
|
||||||
* 支持自定义DSL格式的完整故事系统
|
* 支持自定义DSL格式的完整故事系统
|
||||||
@@ -212,6 +214,8 @@ data class GameState(
|
|||||||
val secretsFound: MutableSet<String> = mutableSetOf(),
|
val secretsFound: MutableSet<String> = mutableSetOf(),
|
||||||
val locationsDiscovered: MutableSet<String> = mutableSetOf(),
|
val locationsDiscovered: MutableSet<String> = mutableSetOf(),
|
||||||
val nodesVisited: MutableSet<String> = mutableSetOf(),
|
val nodesVisited: MutableSet<String> = mutableSetOf(),
|
||||||
|
// 跨循环累计的已访问节点(用于全局剧情百分比统计,不随新循环清空)
|
||||||
|
val nodesVisitedLifetime: MutableSet<String> = mutableSetOf(),
|
||||||
val choicesMade: MutableMap<String, String> = mutableMapOf(),
|
val choicesMade: MutableMap<String, String> = mutableMapOf(),
|
||||||
var currentNodeId: String = "",
|
var currentNodeId: String = "",
|
||||||
var health: Int = 100,
|
var health: Int = 100,
|
||||||
@@ -242,4 +246,40 @@ data class GameState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 可序列化的存档/状态快照
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EngineSaveDTO(
|
||||||
|
val currentNodeId: String,
|
||||||
|
val health: Int,
|
||||||
|
val stamina: Int,
|
||||||
|
val trustLevel: Int,
|
||||||
|
val loopCount: Int,
|
||||||
|
val nodesVisited: List<String>,
|
||||||
|
val nodesVisitedLifetime: List<String>,
|
||||||
|
val secretsFound: List<String>,
|
||||||
|
val locationsDiscovered: List<String>,
|
||||||
|
val flags: List<String>,
|
||||||
|
val choicesMade: Map<String, String>,
|
||||||
|
val variables: Map<String, String>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StoryStatusSnapshot(
|
||||||
|
val currentNodeId: String,
|
||||||
|
val health: Int,
|
||||||
|
val stamina: Int,
|
||||||
|
val trustLevel: Int,
|
||||||
|
val loopCount: Int,
|
||||||
|
val nodesVisited: Set<String>,
|
||||||
|
val nodesVisitedLifetime: Set<String>,
|
||||||
|
val secretsFound: Set<String>,
|
||||||
|
val locationsDiscovered: Set<String>,
|
||||||
|
val flags: Set<String>,
|
||||||
|
val choicesMade: Map<String, String>,
|
||||||
|
val variables: Map<String, String>,
|
||||||
|
val totalNodeCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,26 @@ class StoryEngineAdapter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 存档/状态接口透出
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
fun captureStatusSnapshot(): StoryStatusSnapshot {
|
||||||
|
return newStoryManager.captureStatusSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun captureSaveDTO(): EngineSaveDTO {
|
||||||
|
return newStoryManager.captureSaveDTO()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restoreFromDTO(dto: EngineSaveDTO): Boolean {
|
||||||
|
return newStoryManager.restoreFromDTO(dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTotalNodeCount(): Int {
|
||||||
|
return newStoryManager.getAllNodeIds().size
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 旧系统支持
|
// 旧系统支持
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -72,6 +72,97 @@ class StoryManager(
|
|||||||
// 配置
|
// 配置
|
||||||
private var storyConfig: StoryConfig? = null
|
private var storyConfig: StoryConfig? = null
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 对外状态/存档接口
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 捕获当前状态快照(用于UI展示与存档)
|
||||||
|
*/
|
||||||
|
fun captureStatusSnapshot(): StoryStatusSnapshot {
|
||||||
|
val gs = gameState
|
||||||
|
return StoryStatusSnapshot(
|
||||||
|
currentNodeId = gs.currentNodeId,
|
||||||
|
health = gs.health,
|
||||||
|
stamina = gs.stamina,
|
||||||
|
trustLevel = gs.trustLevel,
|
||||||
|
loopCount = gs.loopCount,
|
||||||
|
nodesVisited = gs.nodesVisited.toSet(),
|
||||||
|
nodesVisitedLifetime = gs.nodesVisitedLifetime.toSet(),
|
||||||
|
secretsFound = gs.secretsFound.toSet(),
|
||||||
|
locationsDiscovered = gs.locationsDiscovered.toSet(),
|
||||||
|
flags = gs.flags.toSet(),
|
||||||
|
choicesMade = gs.choicesMade.toMap(),
|
||||||
|
variables = gs.variables.mapValues { it.value.toString() },
|
||||||
|
totalNodeCount = getAllNodeIds().size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造可序列化的存档DTO
|
||||||
|
*/
|
||||||
|
fun captureSaveDTO(): EngineSaveDTO {
|
||||||
|
val s = captureStatusSnapshot()
|
||||||
|
return EngineSaveDTO(
|
||||||
|
currentNodeId = s.currentNodeId,
|
||||||
|
health = s.health,
|
||||||
|
stamina = s.stamina,
|
||||||
|
trustLevel = s.trustLevel,
|
||||||
|
loopCount = s.loopCount,
|
||||||
|
nodesVisited = s.nodesVisited.toList(),
|
||||||
|
nodesVisitedLifetime = s.nodesVisitedLifetime.toList(),
|
||||||
|
secretsFound = s.secretsFound.toList(),
|
||||||
|
locationsDiscovered = s.locationsDiscovered.toList(),
|
||||||
|
flags = s.flags.toList(),
|
||||||
|
choicesMade = s.choicesMade,
|
||||||
|
variables = s.variables
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存档DTO恢复状态(不进行向后兼容)
|
||||||
|
*/
|
||||||
|
suspend fun restoreFromDTO(dto: EngineSaveDTO): Boolean {
|
||||||
|
return withContext(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
gameState.apply {
|
||||||
|
currentNodeId = dto.currentNodeId
|
||||||
|
health = dto.health
|
||||||
|
stamina = dto.stamina
|
||||||
|
trustLevel = dto.trustLevel
|
||||||
|
loopCount = dto.loopCount
|
||||||
|
nodesVisited.clear(); nodesVisited.addAll(dto.nodesVisited)
|
||||||
|
nodesVisitedLifetime.clear(); nodesVisitedLifetime.addAll(dto.nodesVisitedLifetime)
|
||||||
|
secretsFound.clear(); secretsFound.addAll(dto.secretsFound)
|
||||||
|
locationsDiscovered.clear(); locationsDiscovered.addAll(dto.locationsDiscovered)
|
||||||
|
flags.clear(); flags.addAll(dto.flags)
|
||||||
|
choicesMade.clear(); choicesMade.putAll(dto.choicesMade)
|
||||||
|
variables.clear(); variables.putAll(dto.variables)
|
||||||
|
}
|
||||||
|
_gameStateFlow.value = gameState
|
||||||
|
// 导航到存档节点
|
||||||
|
when (val nav = navigateToNode(dto.currentNodeId)) {
|
||||||
|
is NavigationResult.Success -> true
|
||||||
|
is NavigationResult.Error -> false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_error.value = "Restore failed: ${e.message}"
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前已加载模块的所有节点ID集合
|
||||||
|
*/
|
||||||
|
fun getAllNodeIds(): Set<String> {
|
||||||
|
val ids = mutableSetOf<String>()
|
||||||
|
moduleCache.values.forEach { module ->
|
||||||
|
ids.addAll(module.nodes.keys)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 初始化
|
// 初始化
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -280,6 +371,8 @@ class StoryManager(
|
|||||||
_currentNode.value = node
|
_currentNode.value = node
|
||||||
gameState.currentNodeId = resolvedNodeId
|
gameState.currentNodeId = resolvedNodeId
|
||||||
gameState.nodesVisited.add(resolvedNodeId)
|
gameState.nodesVisited.add(resolvedNodeId)
|
||||||
|
// 累计到跨循环集合,用于全局剧情百分比统计
|
||||||
|
gameState.nodesVisitedLifetime.add(resolvedNodeId)
|
||||||
|
|
||||||
// 处理节点效果
|
// 处理节点效果
|
||||||
val effects = processNodeEffects(node)
|
val effects = processNodeEffects(node)
|
||||||
|
|||||||
Reference in New Issue
Block a user