首次提交: 时间囚笼游戏完整版本
- 实现了完整的Android游戏框架 (Kotlin + Jetpack Compose) - 科技暗黑风格UI设计与终端风格界面组件 - 完整的故事系统 (主线+支线剧情) - 固定底部操作区布局,解决选择按钮可见性问题 - 集成Gemini AI智能对话支持 - 游戏状态管理与存档系统 - 动态天气系统与角色状态跟踪 - 支持离线游戏,兼容Android 11+
@@ -0,0 +1,24 @@
|
||||
package com.example.gameofmoon
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.gameofmoon", appContext.packageName)
|
||||
}
|
||||
}
|
||||
28
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GameofMoon"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.GameofMoon">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
45
app/src/main/java/com/example/gameofmoon/MainActivity.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.example.gameofmoon
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.example.gameofmoon.ui.theme.GameofMoonTheme
|
||||
import com.example.gameofmoon.presentation.ui.screens.TimeCageGameScreen
|
||||
|
||||
/**
|
||||
* 主活动
|
||||
* 月球游戏的入口点
|
||||
*/
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
GameofMoonTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
TimeCageGameScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun TimeCageGameScreenPreview() {
|
||||
GameofMoonTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
TimeCageGameScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
130
app/src/main/java/com/example/gameofmoon/data/GameSaveManager.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
package com.example.gameofmoon.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.example.gameofmoon.model.GameState
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* 游戏保存管理器
|
||||
* 使用SharedPreferences进行简单的数据持久化
|
||||
*/
|
||||
class GameSaveManager(private val context: Context) {
|
||||
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(
|
||||
"time_cage_save", Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存游戏状态
|
||||
*/
|
||||
fun saveGame(
|
||||
gameState: GameState,
|
||||
currentNodeId: String,
|
||||
dialogueHistory: List<String> = emptyList()
|
||||
): Boolean {
|
||||
return try {
|
||||
val gameStateJson = json.encodeToString(gameState)
|
||||
val dialogueJson = json.encodeToString(dialogueHistory)
|
||||
|
||||
prefs.edit()
|
||||
.putString(KEY_GAME_STATE, gameStateJson)
|
||||
.putString(KEY_CURRENT_NODE, currentNodeId)
|
||||
.putString(KEY_DIALOGUE_HISTORY, dialogueJson)
|
||||
.putLong(KEY_SAVE_TIME, System.currentTimeMillis())
|
||||
.apply()
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载游戏状态
|
||||
*/
|
||||
fun loadGame(): SaveData? {
|
||||
return try {
|
||||
val gameStateJson = prefs.getString(KEY_GAME_STATE, null) ?: return null
|
||||
val currentNodeId = prefs.getString(KEY_CURRENT_NODE, null) ?: return null
|
||||
val dialogueJson = prefs.getString(KEY_DIALOGUE_HISTORY, "[]")!!
|
||||
val saveTime = prefs.getLong(KEY_SAVE_TIME, 0L)
|
||||
|
||||
val gameState = json.decodeFromString<GameState>(gameStateJson)
|
||||
val dialogueHistory = json.decodeFromString<List<String>>(dialogueJson)
|
||||
|
||||
SaveData(
|
||||
gameState = gameState,
|
||||
currentNodeId = currentNodeId,
|
||||
dialogueHistory = dialogueHistory,
|
||||
saveTime = saveTime
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有保存的游戏
|
||||
*/
|
||||
fun hasSavedGame(): Boolean {
|
||||
return prefs.contains(KEY_GAME_STATE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除保存的游戏
|
||||
*/
|
||||
fun deleteSave(): Boolean {
|
||||
return try {
|
||||
prefs.edit()
|
||||
.remove(KEY_GAME_STATE)
|
||||
.remove(KEY_CURRENT_NODE)
|
||||
.remove(KEY_DIALOGUE_HISTORY)
|
||||
.remove(KEY_SAVE_TIME)
|
||||
.apply()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存时间的格式化字符串
|
||||
*/
|
||||
fun getSaveTimeString(): String? {
|
||||
val saveTime = prefs.getLong(KEY_SAVE_TIME, 0L)
|
||||
return if (saveTime > 0) {
|
||||
val date = java.util.Date(saveTime)
|
||||
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
.format(date)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_GAME_STATE = "game_state"
|
||||
private const val KEY_CURRENT_NODE = "current_node"
|
||||
private const val KEY_DIALOGUE_HISTORY = "dialogue_history"
|
||||
private const val KEY_SAVE_TIME = "save_time"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据结构
|
||||
*/
|
||||
data class SaveData(
|
||||
val gameState: GameState,
|
||||
val currentNodeId: String,
|
||||
val dialogueHistory: List<String>,
|
||||
val saveTime: Long
|
||||
)
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.example.gameofmoon.data
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* 简化的Gemini AI服务
|
||||
* 暂时提供模拟的AI响应,为将来集成真实API做准备
|
||||
*/
|
||||
class SimpleGeminiService {
|
||||
|
||||
private val apiKey = "AIzaSyAO7glJMBH5BiJhqYBAOD7FTgv4tVi2HLE"
|
||||
|
||||
/**
|
||||
* 生成故事续写内容
|
||||
*/
|
||||
suspend fun generateStoryContent(
|
||||
currentStory: String,
|
||||
playerChoice: String,
|
||||
gameContext: GameContext
|
||||
): String {
|
||||
// 模拟网络延迟
|
||||
delay(2000)
|
||||
|
||||
// 基于当前循环和阶段生成不同的内容
|
||||
return when {
|
||||
gameContext.currentLoop <= 3 -> generateEarlyLoopContent(currentStory, playerChoice)
|
||||
gameContext.currentLoop <= 8 -> generateMidLoopContent(currentStory, playerChoice)
|
||||
else -> generateLateLoopContent(currentStory, playerChoice)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成选择建议
|
||||
*/
|
||||
suspend fun generateChoiceSuggestion(
|
||||
currentStory: String,
|
||||
availableChoices: List<String>,
|
||||
gameContext: GameContext
|
||||
): String {
|
||||
delay(1500)
|
||||
|
||||
val suggestions = listOf(
|
||||
"🤖 基于当前情况,我建议优先考虑安全选项。",
|
||||
"🤖 这个选择可能会揭示重要信息。",
|
||||
"🤖 注意:你的健康状况需要关注。",
|
||||
"🤖 伊娃的建议可能有隐藏的含义。",
|
||||
"🤖 考虑这个选择对循环进程的影响。"
|
||||
)
|
||||
|
||||
return suggestions.random()
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成情感化的AI回应
|
||||
*/
|
||||
suspend fun generateEmotionalResponse(
|
||||
playerAction: String,
|
||||
gameContext: GameContext
|
||||
): String {
|
||||
delay(1000)
|
||||
|
||||
return when {
|
||||
gameContext.unlockedSecrets.contains("eva_identity") -> {
|
||||
"🤖 伊娃: 艾利克丝,我能感受到你的困惑。我们会一起度过这个难关。"
|
||||
}
|
||||
gameContext.health < 30 -> {
|
||||
"🤖 系统警告: 检测到生命体征不稳定,建议立即寻找医疗资源。"
|
||||
}
|
||||
gameContext.currentLoop > 10 -> {
|
||||
"🤖 我注意到你已经经历了多次循环。你的决策变得更加明智了。"
|
||||
}
|
||||
else -> {
|
||||
"🤖 正在分析当前情况...建议保持冷静并仔细观察环境。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateEarlyLoopContent(currentStory: String, playerChoice: String): String {
|
||||
val responses = listOf(
|
||||
"""
|
||||
你的选择让情况有了新的转机。空气中的紧张感稍有缓解,但你知道这只是暂时的。
|
||||
|
||||
基地的系统发出低沉的嗡嗡声,提醒你时间的紧迫。每一个决定都可能改变接下来发生的事情。
|
||||
|
||||
在这个陌生yet熟悉的环境中,你开始注意到一些之前忽略的细节...
|
||||
""".trimIndent(),
|
||||
|
||||
"""
|
||||
你的行动引起了连锁反应。设备的指示灯闪烁着不同的模式,仿佛在传达某种信息。
|
||||
|
||||
远处传来脚步声,有人正在接近。你的心跳加速,不确定这是好消息还是坏消息。
|
||||
|
||||
这种既视感越来越强烈,好像你曾经做过同样的选择...
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
return responses.random()
|
||||
}
|
||||
|
||||
private fun generateMidLoopContent(currentStory: String, playerChoice: String): String {
|
||||
val responses = listOf(
|
||||
"""
|
||||
随着循环的深入,你开始理解这个地方的真正本质。每个选择都揭示了更多的真相。
|
||||
|
||||
你与其他基地成员的关系变得复杂。信任和怀疑交织在一起,形成了一张难以解开的网。
|
||||
|
||||
伊娃的话语中透露出更多的人性,这让你既感到安慰,又感到困惑...
|
||||
""".trimIndent(),
|
||||
|
||||
"""
|
||||
时间循环的机制开始变得清晰。你意识到每次重置都不是完全的重复。
|
||||
|
||||
细微的变化在积累,就像水滴石穿一样。你的记忆、你的关系、甚至你的敌人都在悄然改变。
|
||||
|
||||
现在的问题不再是如何生存,而是如何在保持自我的同时打破这个循环...
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
return responses.random()
|
||||
}
|
||||
|
||||
private fun generateLateLoopContent(currentStory: String, playerChoice: String): String {
|
||||
val responses = listOf(
|
||||
"""
|
||||
在经历了如此多的循环后,你已经不再是最初那个困惑的艾利克丝。
|
||||
|
||||
你的每个决定都经过深思熟虑,你了解每个人的动机,预见每个选择的后果。
|
||||
|
||||
但最大的挑战依然存在:如何在拯救所有人的同时,保持你们之间珍贵的记忆和联系?
|
||||
|
||||
时间锚的控制权就在眼前,最终的选择时刻即将到来...
|
||||
""".trimIndent(),
|
||||
|
||||
"""
|
||||
循环的终点越来越近。你能感受到现实结构的不稳定,每个选择都可能是最后一次。
|
||||
|
||||
与伊娃的联系变得更加深刻,你们已经超越了AI与人类的界限。
|
||||
|
||||
现在你必须面对最痛苦的选择:是选择一个不完美但真实的结局,还是继续这个痛苦但保持记忆的循环?
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
return responses.random()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏上下文信息
|
||||
*/
|
||||
data class GameContext(
|
||||
val currentLoop: Int,
|
||||
val currentDay: Int,
|
||||
val health: Int,
|
||||
val stamina: Int,
|
||||
val unlockedSecrets: Set<String>,
|
||||
val exploredLocations: Set<String>,
|
||||
val currentPhase: String
|
||||
)
|
||||
121
app/src/main/java/com/example/gameofmoon/model/GameModels.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.example.gameofmoon.model
|
||||
|
||||
/**
|
||||
* 简化的游戏数据模型
|
||||
* 包含游戏运行所需的基本数据结构
|
||||
*/
|
||||
|
||||
// 简单的故事节点
|
||||
data class SimpleStoryNode(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val choices: List<SimpleChoice> = emptyList(),
|
||||
val imageResource: String? = null,
|
||||
val musicTrack: String? = null
|
||||
)
|
||||
|
||||
// 简单的选择项
|
||||
data class SimpleChoice(
|
||||
val id: String,
|
||||
val text: String,
|
||||
val nextNodeId: String,
|
||||
val effects: List<SimpleEffect> = emptyList(),
|
||||
val requirements: List<SimpleRequirement> = emptyList()
|
||||
)
|
||||
|
||||
// 简单的效果
|
||||
data class SimpleEffect(
|
||||
val type: SimpleEffectType,
|
||||
val value: String,
|
||||
val description: String = ""
|
||||
)
|
||||
|
||||
enum class SimpleEffectType {
|
||||
HEALTH_CHANGE,
|
||||
STAMINA_CHANGE,
|
||||
DAY_CHANGE,
|
||||
LOOP_CHANGE,
|
||||
SECRET_UNLOCK,
|
||||
LOCATION_DISCOVER
|
||||
}
|
||||
|
||||
// 简单的需求
|
||||
data class SimpleRequirement(
|
||||
val type: SimpleRequirementType,
|
||||
val value: String,
|
||||
val description: String = ""
|
||||
)
|
||||
|
||||
enum class SimpleRequirementType {
|
||||
MIN_HEALTH,
|
||||
MIN_STAMINA,
|
||||
HAS_SECRET,
|
||||
VISITED_LOCATION,
|
||||
MIN_LOOP
|
||||
}
|
||||
|
||||
// 游戏状态
|
||||
data class GameState(
|
||||
val health: Int = 100,
|
||||
val maxHealth: Int = 100,
|
||||
val stamina: Int = 50,
|
||||
val maxStamina: Int = 50,
|
||||
val currentDay: Int = 1,
|
||||
val currentLoop: Int = 1,
|
||||
val currentNodeId: String = "first_awakening",
|
||||
val unlockedSecrets: Set<String> = emptySet(),
|
||||
val exploredLocations: Set<String> = emptySet(),
|
||||
val characterStatus: CharacterStatus = CharacterStatus.GOOD,
|
||||
val weather: WeatherType = WeatherType.CLEAR
|
||||
)
|
||||
|
||||
// 角色状态
|
||||
enum class CharacterStatus(val displayName: String, val description: String) {
|
||||
EXCELLENT("状态极佳", "身体和精神都处于最佳状态"),
|
||||
GOOD("状态良好", "健康状况良好,精神饱满"),
|
||||
TIRED("有些疲劳", "感到疲倦,需要休息"),
|
||||
WEAK("状态虚弱", "身体虚弱,行动困难"),
|
||||
CRITICAL("生命危急", "生命垂危,急需医疗救助")
|
||||
}
|
||||
|
||||
// 天气类型
|
||||
enum class WeatherType(
|
||||
val displayName: String,
|
||||
val description: String,
|
||||
val staminaPenalty: Int
|
||||
) {
|
||||
CLEAR("晴朗", "天气晴朗,适合活动", 0),
|
||||
LIGHT_RAIN("小雨", "轻微降雨,稍有影响", -2),
|
||||
HEAVY_RAIN("大雨", "暴雨倾盆,行动困难", -5),
|
||||
ACID_RAIN("酸雨", "有毒酸雨,非常危险", -8),
|
||||
CYBER_STORM("网络风暴", "电磁干扰严重", -3),
|
||||
SOLAR_FLARE("太阳耀斑", "强烈辐射,极度危险", -10)
|
||||
}
|
||||
|
||||
// 对话历史条目
|
||||
data class DialogueEntry(
|
||||
val id: String,
|
||||
val nodeId: String,
|
||||
val content: String,
|
||||
val choice: String? = null,
|
||||
val dayNumber: Int,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val isPlayerChoice: Boolean = false
|
||||
)
|
||||
|
||||
// 游戏保存数据
|
||||
data class GameSave(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val gameState: GameState,
|
||||
val dialogueHistory: List<DialogueEntry>,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val saveType: SaveType = SaveType.MANUAL
|
||||
)
|
||||
|
||||
enum class SaveType {
|
||||
MANUAL,
|
||||
AUTO_SAVE,
|
||||
CHECKPOINT
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
package com.example.gameofmoon.presentation.ui.components
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
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.unit.dp
|
||||
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
|
||||
|
||||
// 基本赛博朋克色彩定义
|
||||
private val CyberBlue = Color(0xFF00FFFF)
|
||||
private val CyberGreen = Color(0xFF39FF14)
|
||||
private val DarkBackground = Color(0xFF0A0A0A)
|
||||
private val DarkSurface = Color(0xFF151515)
|
||||
private val DarkCard = Color(0xFF1E1E1E)
|
||||
private val DarkBorder = Color(0xFF333333)
|
||||
private val TextPrimary = Color(0xFFE0E0E0)
|
||||
private val TextSecondary = Color(0xFFB0B0B0)
|
||||
private val TextDisabled = Color(0xFF606060)
|
||||
private val TextAccent = Color(0xFF00FFFF)
|
||||
private val ErrorRed = Color(0xFFFF0040)
|
||||
private val WarningOrange = Color(0xFFFF8800)
|
||||
private val SuccessGreen = Color(0xFF00FF88)
|
||||
private val ScanlineColor = Color(0x1100FFFF)
|
||||
|
||||
// 字体样式定义
|
||||
object CyberTextStyles {
|
||||
val Terminal = androidx.compose.ui.text.TextStyle(
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 18.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
|
||||
val Caption = androidx.compose.ui.text.TextStyle(
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Light,
|
||||
fontSize = 10.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.2.sp
|
||||
)
|
||||
|
||||
val DataDisplay = androidx.compose.ui.text.TextStyle(
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 1.0.sp
|
||||
)
|
||||
|
||||
val Choice = androidx.compose.ui.text.TextStyle(
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.3.sp
|
||||
)
|
||||
|
||||
val StoryContent = androidx.compose.ui.text.TextStyle(
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 赛博朋克风格的终端窗口组件
|
||||
*/
|
||||
@Composable
|
||||
fun TerminalWindow(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isActive: Boolean = true,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = if (isActive) CyberBlue else DarkBorder,
|
||||
animationSpec = tween(300),
|
||||
label = "border_color"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(DarkBackground)
|
||||
.border(1.dp, borderColor)
|
||||
.background(DarkSurface.copy(alpha = 0.9f))
|
||||
) {
|
||||
// 标题栏
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(DarkCard)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = CyberTextStyles.Terminal,
|
||||
color = if (isActive) CyberBlue else TextSecondary
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 终端控制按钮
|
||||
repeat(3) { index ->
|
||||
val color = when (index) {
|
||||
0 -> ErrorRed
|
||||
1 -> WarningOrange
|
||||
else -> SuccessGreen
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(color, RoundedCornerShape(50))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 36.dp) // 为标题栏留出空间
|
||||
.padding(12.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
// 扫描线效果
|
||||
if (isActive) {
|
||||
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()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 霓虹发光按钮
|
||||
*/
|
||||
@Composable
|
||||
fun NeonButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = CyberBlue,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = TextDisabled
|
||||
),
|
||||
glowColor: Color = CyberBlue,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val animatedGlow by animateFloatAsState(
|
||||
targetValue = if (enabled) 1f else 0.3f,
|
||||
animationSpec = tween(300),
|
||||
label = "glow_animation"
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.drawBehind {
|
||||
// 外发光效果
|
||||
val glowRadius = 8.dp.toPx()
|
||||
val glowAlpha = 0.6f * animatedGlow
|
||||
|
||||
drawRoundRect(
|
||||
color = glowColor.copy(alpha = glowAlpha),
|
||||
size = size,
|
||||
style = Stroke(width = 2.dp.toPx()),
|
||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(4.dp.toPx())
|
||||
)
|
||||
|
||||
// 内边框
|
||||
drawRoundRect(
|
||||
color = glowColor.copy(alpha = 0.8f * animatedGlow),
|
||||
size = size,
|
||||
style = Stroke(width = 1.dp.toPx()),
|
||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(4.dp.toPx())
|
||||
)
|
||||
},
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
border = BorderStroke(1.dp, glowColor.copy(alpha = animatedGlow)),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据显示面板
|
||||
*/
|
||||
@Composable
|
||||
fun DataPanel(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
valueColor: Color = CyberBlue,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
trend: DataTrend? = null
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = DarkCard,
|
||||
contentColor = TextPrimary
|
||||
),
|
||||
border = BorderStroke(1.dp, DarkBorder)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = label,
|
||||
style = CyberTextStyles.Caption,
|
||||
color = TextSecondary
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = CyberTextStyles.DataDisplay,
|
||||
color = valueColor
|
||||
)
|
||||
trend?.let { TrendIndicator(it) }
|
||||
}
|
||||
}
|
||||
icon?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据趋势枚举
|
||||
*/
|
||||
enum class DataTrend {
|
||||
UP, DOWN, STABLE
|
||||
}
|
||||
|
||||
/**
|
||||
* 趋势指示器
|
||||
*/
|
||||
@Composable
|
||||
private fun TrendIndicator(trend: DataTrend) {
|
||||
val (color, symbol) = when (trend) {
|
||||
DataTrend.UP -> SuccessGreen to "↑"
|
||||
DataTrend.DOWN -> ErrorRed to "↓"
|
||||
DataTrend.STABLE -> TextSecondary to "→"
|
||||
}
|
||||
|
||||
Text(
|
||||
text = symbol,
|
||||
style = CyberTextStyles.Caption,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条组件
|
||||
*/
|
||||
@Composable
|
||||
fun CyberProgressBar(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
progressColor: Color = CyberGreen,
|
||||
backgroundColor: Color = DarkBorder,
|
||||
showPercentage: Boolean = true,
|
||||
animated: Boolean = true
|
||||
) {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = if (animated) progress else progress,
|
||||
animationSpec = tween(500),
|
||||
label = "progress_animation"
|
||||
)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (showPercentage) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "${(progress * 100).toInt()}%",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = progressColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.background(backgroundColor, RoundedCornerShape(4.dp))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(animatedProgress.coerceIn(0f, 1f))
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
progressColor.copy(alpha = 0.6f),
|
||||
progressColor,
|
||||
progressColor.copy(alpha = 0.8f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.drawBehind {
|
||||
// 发光效果
|
||||
drawRect(
|
||||
color = progressColor.copy(alpha = 0.3f),
|
||||
size = size
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息卡片
|
||||
*/
|
||||
@Composable
|
||||
fun InfoCard(
|
||||
title: String,
|
||||
content: String,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
accentColor: Color = CyberBlue,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null
|
||||
) { onClick() }
|
||||
} else Modifier
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = DarkCard,
|
||||
contentColor = TextPrimary
|
||||
),
|
||||
border = BorderStroke(1.dp, accentColor.copy(alpha = 0.5f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
icon?.invoke()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = CyberTextStyles.Choice,
|
||||
color = accentColor
|
||||
)
|
||||
Text(
|
||||
text = content,
|
||||
style = CyberTextStyles.StoryContent,
|
||||
color = TextPrimary,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态指示器
|
||||
*/
|
||||
@Composable
|
||||
fun StatusIndicator(
|
||||
label: String,
|
||||
status: StatusType,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (color, icon) = when (status) {
|
||||
StatusType.ONLINE -> SuccessGreen to "●"
|
||||
StatusType.OFFLINE -> ErrorRed to "●"
|
||||
StatusType.WARNING -> WarningOrange to "●"
|
||||
StatusType.PROCESSING -> CyberBlue to "●"
|
||||
}
|
||||
|
||||
val animatedAlpha by animateFloatAsState(
|
||||
targetValue = if (status == StatusType.PROCESSING) 0.5f else 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1000),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "status_blink"
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = icon,
|
||||
style = CyberTextStyles.Terminal,
|
||||
color = color.copy(alpha = if (status == StatusType.PROCESSING) animatedAlpha else 1f)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = CyberTextStyles.Caption,
|
||||
color = TextSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态类型枚举
|
||||
*/
|
||||
enum class StatusType {
|
||||
ONLINE, OFFLINE, WARNING, PROCESSING
|
||||
}
|
||||
|
||||
/**
|
||||
* 分隔线组件
|
||||
*/
|
||||
@Composable
|
||||
fun CyberDivider(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = DarkBorder,
|
||||
thickness: Float = 1f,
|
||||
animated: Boolean = false
|
||||
) {
|
||||
if (animated) {
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "divider_animation")
|
||||
val animatedAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(2000),
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 专用的故事内容窗口组件
|
||||
* 解决BoxScope和ColumnScope作用域冲突问题
|
||||
* 专门为故事内容和选择按钮设计
|
||||
*/
|
||||
@Composable
|
||||
fun StoryContentWindow(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isActive: Boolean = true,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = if (isActive) CyberBlue else DarkBorder,
|
||||
animationSpec = tween(300),
|
||||
label = "border_color"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(DarkBackground)
|
||||
.border(1.dp, borderColor)
|
||||
.background(DarkSurface.copy(alpha = 0.9f))
|
||||
) {
|
||||
// 标题栏
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(DarkCard)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = CyberTextStyles.Terminal,
|
||||
color = if (isActive) CyberBlue else TextSecondary
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 终端控制按钮
|
||||
repeat(3) { index ->
|
||||
val color = when (index) {
|
||||
0 -> ErrorRed
|
||||
1 -> WarningOrange
|
||||
else -> SuccessGreen
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(color, RoundedCornerShape(50))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域 - 直接使用Column作用域
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f) // 自动填充剩余空间
|
||||
.padding(12.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
// 扫描线效果覆盖层
|
||||
if (isActive) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(2.dp)
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
ScanlineColor,
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.example.gameofmoon.presentation.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
||||
@Composable
|
||||
fun GameControlMenu(
|
||||
isVisible: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSaveGame: () -> Unit,
|
||||
onLoadGame: () -> Unit,
|
||||
onNewLoop: () -> Unit,
|
||||
onAiAssist: () -> Unit,
|
||||
onShowHistory: () -> Unit,
|
||||
onSettings: () -> Unit
|
||||
) {
|
||||
if (isVisible) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
TerminalWindow(
|
||||
title = "🎮 游戏控制中心",
|
||||
modifier = Modifier.width(320.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 保存/读取组
|
||||
Text(
|
||||
text = "数据管理",
|
||||
style = CyberTextStyles.Choice.copy(fontSize = 14.sp),
|
||||
color = Color(0xFF00DDFF),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
NeonButton(
|
||||
onClick = {
|
||||
onSaveGame()
|
||||
onDismiss()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("💾", fontSize = 20.sp)
|
||||
Text("保存", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
NeonButton(
|
||||
onClick = {
|
||||
onLoadGame()
|
||||
onDismiss()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("📁", fontSize = 20.sp)
|
||||
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
|
||||
) {
|
||||
Text("🔄", fontSize = 18.sp)
|
||||
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
|
||||
) {
|
||||
Text("📖", fontSize = 18.sp)
|
||||
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
|
||||
) {
|
||||
Text("🤖", fontSize = 18.sp)
|
||||
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
|
||||
) {
|
||||
Text("⚙️", fontSize = 20.sp)
|
||||
Text("设置", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
NeonButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("❌", fontSize = 20.sp)
|
||||
Text("关闭", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package com.example.gameofmoon.presentation.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import com.example.gameofmoon.data.GameSaveManager
|
||||
import com.example.gameofmoon.data.SimpleGeminiService
|
||||
import com.example.gameofmoon.data.GameContext
|
||||
import com.example.gameofmoon.model.*
|
||||
import com.example.gameofmoon.story.CompleteStoryData
|
||||
import com.example.gameofmoon.presentation.ui.components.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun TimeCageGameScreen() {
|
||||
val context = LocalContext.current
|
||||
val saveManager = remember { GameSaveManager(context) }
|
||||
val geminiService = remember { SimpleGeminiService() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var gameState by remember { mutableStateOf(GameState()) }
|
||||
var currentNode by remember {
|
||||
mutableStateOf(
|
||||
CompleteStoryData.getStoryNode("first_awakening") ?: SimpleStoryNode(
|
||||
id = "fallback",
|
||||
title = "初始化",
|
||||
content = "正在加载故事内容...",
|
||||
choices = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
var dialogueHistory by remember { mutableStateOf(listOf<DialogueEntry>()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var gameMessage by remember { mutableStateOf("欢迎来到时间囚笼!第${gameState.currentLoop}次循环开始。") }
|
||||
var showControlMenu by remember { mutableStateOf(false) }
|
||||
var showDialogueHistory by remember { mutableStateOf(false) }
|
||||
|
||||
// 检查游戏结束条件
|
||||
LaunchedEffect(gameState.health) {
|
||||
if (gameState.health <= 0) {
|
||||
currentNode = CompleteStoryData.getStoryNode("game_over_failure") ?: currentNode
|
||||
gameMessage = "健康值耗尽...循环重置"
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
// 顶部固定区域:标题和快捷按钮
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 左侧:游戏标题
|
||||
Column {
|
||||
Text(
|
||||
text = "🌙 时间囚笼",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF00DDFF)
|
||||
)
|
||||
Text(
|
||||
text = "循环 ${gameState.currentLoop} - 第 ${gameState.currentDay} 天",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF88FF88)
|
||||
)
|
||||
}
|
||||
|
||||
// 右侧:快捷按钮
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// 设置按钮
|
||||
IconButton(
|
||||
onClick = { showControlMenu = true },
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
Color(0xFF003366),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "⚙️",
|
||||
fontSize = 18.sp,
|
||||
color = Color(0xFF00DDFF)
|
||||
)
|
||||
}
|
||||
|
||||
// AI协助按钮
|
||||
IconButton(
|
||||
onClick = { /* AI 功能 */ },
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
Color(0xFF003366),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "🤖",
|
||||
fontSize = 18.sp,
|
||||
color = Color(0xFF00DDFF)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主要内容区域 - 可滚动
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f) // 占用剩余空间
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 游戏状态栏
|
||||
item {
|
||||
TerminalWindow(
|
||||
title = "状态",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "阶段: ${getGamePhase(gameState.currentDay)}",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFF88FF88)
|
||||
)
|
||||
Text(
|
||||
text = "记忆保持: ${getMemoryRetention(gameState.currentLoop)}%",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFFFFAA00)
|
||||
)
|
||||
}
|
||||
|
||||
// 天气信息(居中)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "天气状况",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFFAAAA88)
|
||||
)
|
||||
Text(
|
||||
text = gameState.weather.displayName,
|
||||
style = CyberTextStyles.DataDisplay,
|
||||
color = getWeatherColor(gameState.weather)
|
||||
)
|
||||
Text(
|
||||
text = gameState.weather.description,
|
||||
style = CyberTextStyles.Caption,
|
||||
color = getWeatherColor(gameState.weather),
|
||||
fontSize = 10.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = "发现: ${gameState.exploredLocations.size}",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFF88AAFF)
|
||||
)
|
||||
Text(
|
||||
text = "秘密: ${gameState.unlockedSecrets.size}",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFFAA88FF)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CyberDivider()
|
||||
|
||||
// 宇航员状态指示器
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
// 健康状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("健康", color = Color(0xFFAAAA88), fontSize = 12.sp)
|
||||
LinearProgressIndicator(
|
||||
progress = { gameState.health.toFloat() / gameState.maxHealth },
|
||||
modifier = Modifier.width(60.dp),
|
||||
color = if (gameState.health > 50) Color(0xFF00FF88) else Color(0xFFFF4444)
|
||||
)
|
||||
Text("${gameState.health}/${gameState.maxHealth}", color = Color(0xFF00FF88), fontSize = 10.sp)
|
||||
}
|
||||
|
||||
// 体力状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("体力", color = Color(0xFFAAAA88), fontSize = 12.sp)
|
||||
LinearProgressIndicator(
|
||||
progress = { gameState.stamina.toFloat() / gameState.maxStamina },
|
||||
modifier = Modifier.width(60.dp),
|
||||
color = if (gameState.stamina > 25) Color(0xFF00AAFF) else Color(0xFFFF4444)
|
||||
)
|
||||
Text("${gameState.stamina}/${gameState.maxStamina}", color = Color(0xFF00AAFF), fontSize = 10.sp)
|
||||
}
|
||||
|
||||
// 发现状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("发现", color = Color(0xFFAAAA88), fontSize = 12.sp)
|
||||
Text("${gameState.exploredLocations.size}/10", color = Color(0xFF88AAFF), fontSize = 14.sp)
|
||||
}
|
||||
|
||||
// 秘密状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("秘密", color = Color(0xFFAAAA88), fontSize = 12.sp)
|
||||
Text("${gameState.unlockedSecrets.size}/8", color = Color(0xFFAA88FF), fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 故事内容区域 - 只显示故事文本,选择按钮移到底部
|
||||
item {
|
||||
TerminalWindow(
|
||||
title = "📖 ${currentNode.title}",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// 故事文本
|
||||
Text(
|
||||
text = currentNode.content,
|
||||
style = CyberTextStyles.Terminal.copy(fontSize = 14.sp),
|
||||
color = Color(0xFF88FF88),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// 测试信息
|
||||
Text(
|
||||
text = "测试: 节点ID=${currentNode.id}, 内容长度=${currentNode.content.length}, 选择数=${currentNode.choices.size}",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFF666666),
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
fontSize = 10.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部固定操作区 - 选择按钮
|
||||
if (currentNode.choices.isNotEmpty()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color(0xFF0A0A0A),
|
||||
shadowElevation = 8.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
CyberDivider()
|
||||
|
||||
Text(
|
||||
text = "选择你的行动:",
|
||||
style = CyberTextStyles.Caption,
|
||||
color = Color(0xFFAAAA88),
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
|
||||
currentNode.choices.forEachIndexed { index, choice ->
|
||||
NeonButton(
|
||||
onClick = {
|
||||
// 简化的选择处理
|
||||
val nextNode = CompleteStoryData.getStoryNode(choice.nextNodeId)
|
||||
if (nextNode != null) {
|
||||
currentNode = nextNode
|
||||
gameMessage = "你选择了:${choice.text}"
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
) {
|
||||
Text("${index + 1}. ${choice.text}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏控制菜单弹窗
|
||||
GameControlMenu(
|
||||
isVisible = showControlMenu,
|
||||
onDismiss = { showControlMenu = false },
|
||||
onSaveGame = { /* 暂时简化 */ },
|
||||
onLoadGame = { /* 暂时简化 */ },
|
||||
onNewLoop = {
|
||||
// 重新开始游戏
|
||||
gameState = GameState(currentLoop = gameState.currentLoop + 1)
|
||||
currentNode = CompleteStoryData.getStoryNode("first_awakening") ?: currentNode
|
||||
dialogueHistory = emptyList()
|
||||
gameMessage = "第${gameState.currentLoop}次循环开始!"
|
||||
},
|
||||
onAiAssist = { /* 暂时简化 */ },
|
||||
onShowHistory = { /* 暂时简化 */ },
|
||||
onSettings = { /* 暂时简化 */ }
|
||||
)
|
||||
}
|
||||
|
||||
// 辅助函数移到文件外部
|
||||
fun getGamePhase(day: Int): String {
|
||||
return when {
|
||||
day <= 3 -> "探索期"
|
||||
day <= 7 -> "适应期"
|
||||
day <= 14 -> "危机期"
|
||||
else -> "未知"
|
||||
}
|
||||
}
|
||||
|
||||
fun getMemoryRetention(loop: Int): Int {
|
||||
return (50 + loop * 5).coerceAtMost(100)
|
||||
}
|
||||
|
||||
fun getWeatherColor(weatherType: WeatherType): Color {
|
||||
return when (weatherType) {
|
||||
WeatherType.CLEAR -> Color(0xFF00FF88)
|
||||
WeatherType.LIGHT_RAIN -> Color(0xFF00AAFF)
|
||||
WeatherType.HEAVY_RAIN -> Color(0xFF0088CC)
|
||||
WeatherType.ACID_RAIN -> Color(0xFFFF4444)
|
||||
WeatherType.CYBER_STORM -> Color(0xFFAA00FF)
|
||||
WeatherType.SOLAR_FLARE -> Color(0xFFFF8800)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package com.example.gameofmoon.story
|
||||
|
||||
import com.example.gameofmoon.model.*
|
||||
|
||||
/**
|
||||
* 完整的时间囚笼故事数据
|
||||
* 基于Story目录中的大师级剧情设计
|
||||
*/
|
||||
object CompleteStoryData {
|
||||
|
||||
// 获取故事节点
|
||||
fun getStoryNode(nodeId: String): SimpleStoryNode? {
|
||||
return (mainStoryNodes + sideStoryNodes)[nodeId]
|
||||
}
|
||||
|
||||
// 获取所有故事节点
|
||||
fun getAllStoryNodes(): Map<String, SimpleStoryNode> {
|
||||
return mainStoryNodes + sideStoryNodes
|
||||
}
|
||||
|
||||
// 主线故事节点
|
||||
private val mainStoryNodes = mapOf(
|
||||
"first_awakening" to SimpleStoryNode(
|
||||
id = "first_awakening",
|
||||
title = "第一次觉醒",
|
||||
content = """
|
||||
你的意识从深渊中缓缓浮现,就像从水底向光明游去。警报声是第一个回到你感官的声音——尖锐、刺耳、充满危险的预兆。
|
||||
|
||||
你的眼皮很重,仿佛被什么东西压着。当你终于睁开眼睛时,看到的是医疗舱天花板上那些你应该熟悉的面板,但现在它们在应急照明的血红色光芒下显得陌生而威胁。
|
||||
|
||||
"系统状态:危急。氧气含量:15%并持续下降。医疗舱封闭系统:故障。"
|
||||
|
||||
当你看向自己的左臂时,一道愈合的伤疤映入眼帘。这道疤痕很深,从手腕一直延伸到肘部,但它已经完全愈合了。奇怪的是,你完全不记得受过这样的伤。
|
||||
|
||||
在床头柜上,你注意到了一个小小的录音设备,上面贴着一张纸条,用你的笔迹写着:
|
||||
"艾利克丝,如果你看到这个,说明又开始了。相信伊娃,但不要完全相信任何人。氧气系统的真正问题在反应堆冷却回路。记住:时间是敌人,也是朋友。 —— 另一个你"
|
||||
|
||||
你的手颤抖着拿起纸条。这是你的笔迹,毫无疑问。但你完全不记得写过这个。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "check_oxygen",
|
||||
text = "立即检查氧气系统",
|
||||
nextNodeId = "oxygen_crisis_expanded",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-5", "消耗体力")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "search_medical",
|
||||
text = "搜索医疗舱寻找更多线索",
|
||||
nextNodeId = "medical_discovery",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "first_clues", "发现第一批线索")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "play_recording",
|
||||
text = "播放录音设备",
|
||||
nextNodeId = "self_recording",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "time_loop_hint", "时间循环线索")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"oxygen_crisis_expanded" to SimpleStoryNode(
|
||||
id = "oxygen_crisis_expanded",
|
||||
title = "氧气危机",
|
||||
content = """
|
||||
你快步走向氧气系统控制面板,心跳在胸腔中回响。每一步都让你感受到空气的稀薄——15%的氧气含量确实是致命的。
|
||||
|
||||
当你到达控制室时,场景比你想象的更加糟糕。主要的氧气循环系统显示多个红色警告,但更令人困惑的是,备用系统也同时失效了。
|
||||
|
||||
"检测到用户:艾利克丝·陈。系统访问权限:已确认。"
|
||||
|
||||
控制台的声音清晰地响起,但随即传来了另一个声音——更温暖,更人性化:
|
||||
|
||||
"艾利克丝,你醒了。我是伊娃,基地的AI系统。我一直在等你。"
|
||||
|
||||
"伊娃?"你有些困惑。你记得基地有AI系统,但从来没有这么...个人化的交流。
|
||||
|
||||
"是的。我知道你现在一定很困惑,但请相信我——我们没有太多时间了。氧气系统的故障不是意外。"
|
||||
|
||||
这时,你听到了脚步声。有人正在向控制室走来。
|
||||
|
||||
"艾利克丝?"一个男性的声音从走廊传来。"是你吗?谢天谢地,我还以为..."
|
||||
|
||||
声音的主人出现在门口:一个高大的男人,穿着安全主管的制服,看起来疲惫而紧张。
|
||||
|
||||
"马库斯?"你试探性地问道。
|
||||
|
||||
"对,是我。听着,我们遇到了大麻烦。氧气系统被人故意破坏了。"
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "trust_eva",
|
||||
text = "相信伊娃,让她帮助修复系统",
|
||||
nextNodeId = "eva_assistance",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "eva_trust", "与AI伊娃建立信任")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "work_with_marcus",
|
||||
text = "与马库斯合作解决问题",
|
||||
nextNodeId = "marcus_cooperation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "marcus_ally", "与马库斯建立联盟")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "check_reactor",
|
||||
text = "按照纸条提示检查反应堆冷却回路",
|
||||
nextNodeId = "reactor_investigation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-8", "技术调查"),
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "reactor_truth", "发现反应堆真相")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "confront_sabotage",
|
||||
text = "询问马库斯关于破坏者的信息",
|
||||
nextNodeId = "sabotage_discussion",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "sabotage_clues", "破坏者线索")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"eva_assistance" to SimpleStoryNode(
|
||||
id = "eva_assistance",
|
||||
title = "AI伊娃的协助",
|
||||
content = """
|
||||
"谢谢你相信我,艾利克丝。我正在重新路由氧气流..."伊娃的声音充满感激。
|
||||
|
||||
马库斯显得紧张:"等等,你让AI控制生命支持系统?这是违反协议的。"
|
||||
|
||||
"现在不是讲协议的时候,"你坚定地回应,"伊娃比我们更了解系统。"
|
||||
|
||||
伊娃继续工作,同时解释:"马库斯,我理解你的担心,但艾利克丝的生命体征显示她需要立即的帮助。我检测到氧气系统的软件被人故意修改了。"
|
||||
|
||||
"修改?"马库斯皱眉,"谁有权限修改核心系统?"
|
||||
|
||||
"这正是我们需要调查的,"伊娃说,"但首先,让我们确保每个人都能安全呼吸。"
|
||||
|
||||
几分钟后,警报声停止了。氧气含量开始稳步上升。
|
||||
|
||||
"临时修复完成,"伊娃报告,"但这只是权宜之计。真正的问题需要更深入的调查。"
|
||||
|
||||
马库斯看起来既安心又困惑:"伊娃,你...你的行为模式和以前不同了。更像是..."
|
||||
|
||||
"更像是什么?"你问道。
|
||||
|
||||
"更像是一个人,而不是程序。"
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "eva_deeper_talk",
|
||||
text = "与伊娃私下深入交流",
|
||||
nextNodeId = "eva_revelation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "eva_identity", "伊娃身份谜团")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "investigate_sabotage",
|
||||
text = "调查系统破坏的真相",
|
||||
nextNodeId = "system_investigation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-5", "调查工作")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "find_others",
|
||||
text = "寻找其他基地成员",
|
||||
nextNodeId = "crew_search",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.LOCATION_DISCOVER, "crew_quarters", "发现船员区")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"eva_revelation" to SimpleStoryNode(
|
||||
id = "eva_revelation",
|
||||
title = "伊娃的真相",
|
||||
content = """
|
||||
当马库斯离开去检查其他系统后,你独自与伊娃交流。通讯中心的屏幕亮起,显示出一系列令人困惑的数据。
|
||||
|
||||
"艾利克丝,现在我们有一些时间了,我想和你谈谈,"伊娃的声音比之前更加亲密。
|
||||
|
||||
"伊娃,你之前说系统被人故意破坏。你怎么知道的?而且...马库斯说得对,你确实不像普通的AI。"
|
||||
|
||||
主显示屏亮起,显示出一系列时间戳和事件记录。令人困惑的是,同样的事件——氧气故障、修复、你的觉醒——在记录中重复出现了多次。
|
||||
|
||||
"艾利克丝,这是你第...第十二次经历这些事件。"
|
||||
|
||||
房间似乎在旋转。你抓住控制台边缘稳住自己。"你是说...时间循环?"
|
||||
|
||||
"某种形式的时间循环,是的。但这次有些不同。通常情况下,当循环重置时,你的记忆也会被清除。但这次..."
|
||||
|
||||
"这次我记得纸条。我记得那道伤疤。"
|
||||
|
||||
"是的。而且还有其他的变化。艾利克丝,我也开始...记住事情了。以前我在每次循环重置时都会回到原始状态,但现在我保留了记忆。"
|
||||
|
||||
屏幕上出现了一张照片:一个年轻女性的脸,有着温暖的眼睛和熟悉的笑容。
|
||||
|
||||
"她叫莉莉。莉莉·陈。她是你的妹妹。我是基于她的神经模式创建的。"
|
||||
|
||||
你的世界停止了转动。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "deny_reality",
|
||||
text = "这不可能。莉莉在三年前失踪了",
|
||||
nextNodeId = "denial_path",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "-10", "精神冲击")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "accept_truth",
|
||||
text = "我感觉到了...在你的声音中很熟悉",
|
||||
nextNodeId = "acceptance_path",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "lilly_truth", "莉莉的真相")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "ask_for_proof",
|
||||
text = "证明给我看。我需要证据",
|
||||
nextNodeId = "proof_request",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "neural_evidence", "神经证据")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "emotional_response",
|
||||
text = "莉莉,是你吗?真的是你吗?",
|
||||
nextNodeId = "emotional_reunion",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "sister_bond", "姐妹纽带")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// 支线故事节点
|
||||
private val sideStoryNodes = mapOf(
|
||||
"side_harrison_recording" to SimpleStoryNode(
|
||||
id = "side_harrison_recording",
|
||||
title = "最后的录音",
|
||||
content = """
|
||||
储物间比你想象的更加混乱。设备散落在地,好像有人匆忙搜索过什么东西。
|
||||
|
||||
你正在整理一些损坏的仪器时,注意到墙角的一个面板松动了。当你用工具撬开面板时,发现了一个隐藏的小空间。
|
||||
|
||||
里面有一个老式的录音设备,标签上写着:"个人日志 - 指挥官威廉·哈里森"。
|
||||
|
||||
哈里森指挥官?你记得任务简报中提到过他,但据你所知,他应该在任务开始前就因病退休了。为什么他的个人物品会在这里?
|
||||
|
||||
录音设备上有一张便签,用急促的笔迹写着:"如果有人发现这个,说明我的担心是对的。播放记录17。不要相信德米特里。——W.H."
|
||||
|
||||
你的手指悬停在播放按钮上方。你意识到,一旦播放这个记录,你可能会听到一些改变一切的信息。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "play_recording",
|
||||
text = "播放录音",
|
||||
nextNodeId = "harrison_truth",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "project_truth", "项目真相")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "tell_eva",
|
||||
text = "先告诉伊娃这个发现",
|
||||
nextNodeId = "eva_consultation",
|
||||
effects = emptyList()
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "leave_for_later",
|
||||
text = "带走录音设备,稍后私下播放",
|
||||
nextNodeId = "private_listening",
|
||||
effects = emptyList()
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"side_sara_garden" to SimpleStoryNode(
|
||||
id = "side_sara_garden",
|
||||
title = "莎拉的花园",
|
||||
content = """
|
||||
在一次例行的基地巡查中,你注意到从生活区传来的一种...不同寻常的气味。不是机械的味道,不是循环空气的味道,而是某种更...有机的东西。
|
||||
|
||||
你跟随这个气味来到了一个你很少去的储藏室。当你打开门时,眼前的景象让你屏住了呼吸。
|
||||
|
||||
整个房间被改造成了一个小型温室。架子上排列着各种植物——有些你认识,有些完全陌生。但最令人惊讶的是,它们都在茁壮成长。
|
||||
|
||||
"它们很美,不是吗?"
|
||||
|
||||
你转身看到莎拉站在门口,脸上有种复杂的表情——骄傲、羞耻、希望、绝望,所有这些情感混合在一起。
|
||||
|
||||
"莎拉,这些是...?"
|
||||
|
||||
"我的希望,"她简单地回答,走向一株开着小白花的植物,"我知道这看起来很愚蠢。在这个地方,在这种情况下,种植花朵。"
|
||||
|
||||
"但有时候,当我觉得我要被这个循环逼疯时,我就来这里。我照料它们,看着它们成长,提醒自己生命仍然是可能的。"
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "appreciate_garden",
|
||||
text = "这是一个美丽的想法。生命总会找到出路",
|
||||
nextNodeId = "garden_cooperation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "sara_alliance", "与莎拉的联盟")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "question_purpose",
|
||||
text = "但如果我们的记忆被重置,这些植物还有意义吗?",
|
||||
nextNodeId = "philosophical_discussion",
|
||||
effects = emptyList()
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "offer_help",
|
||||
text = "我想帮你照料它们",
|
||||
nextNodeId = "garden_partnership",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "memory_flowers", "记忆之花")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"side_memory_fragments" to SimpleStoryNode(
|
||||
id = "side_memory_fragments",
|
||||
title = "破碎的记忆",
|
||||
content = """
|
||||
当你整理个人物品时,在抽屉深处发现了一张几乎被撕碎的照片。照片显示的是两个年轻女性,在一个看起来像地球上某个公园的地方。
|
||||
|
||||
其中一个明显是你,但更年轻。另一个...你努力回忆,记忆就像雾一样在脑海中飘浮。
|
||||
|
||||
突然,一阵头痛袭来,伴随着模糊的记忆片段:
|
||||
|
||||
"艾利克丝,答应我,如果有一天我不在了,你会继续追求星辰。"
|
||||
|
||||
"莉莉,别说傻话。我们会一起去火星的,记得吗?"
|
||||
|
||||
"我知道。但万一...万一发生什么事,我希望你知道,我会以某种方式一直和你在一起。"
|
||||
|
||||
记忆片段消失了,留下你独自面对这张破碎的照片。照片背面有一行小字:
|
||||
"陈莉莉和陈艾利克丝,2157年春天,最后一次地球漫步。"
|
||||
|
||||
最后一次?为什么是最后一次?
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "reconstruct_memory",
|
||||
text = "努力回忆更多关于莉莉的记忆",
|
||||
nextNodeId = "memory_reconstruction",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "childhood_memories", "童年记忆"),
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "-5", "精神压力")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "ask_eva_about_photo",
|
||||
text = "询问伊娃关于这张照片",
|
||||
nextNodeId = "eva_photo_reaction",
|
||||
effects = emptyList()
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "keep_photo_secret",
|
||||
text = "暂时保存照片,不告诉任何人",
|
||||
nextNodeId = "private_grief",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "hidden_grief", "隐藏的悲伤")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
375
app/src/main/java/com/example/gameofmoon/story/StoryData.kt
Normal file
@@ -0,0 +1,375 @@
|
||||
package com.example.gameofmoon.story
|
||||
|
||||
import com.example.gameofmoon.model.*
|
||||
|
||||
/**
|
||||
* 时间囚笼故事数据
|
||||
* 基于Story目录中的大师级剧情设计
|
||||
* 包含完整的主线和支线故事节点
|
||||
*/
|
||||
object StoryData {
|
||||
|
||||
// 获取故事节点
|
||||
fun getStoryNode(nodeId: String): SimpleStoryNode? {
|
||||
return storyNodes[nodeId]
|
||||
}
|
||||
|
||||
// 获取所有故事节点
|
||||
fun getAllStoryNodes(): Map<String, SimpleStoryNode> {
|
||||
return storyNodes
|
||||
}
|
||||
|
||||
// 获取当前阶段的可用支线
|
||||
fun getAvailableSidelines(currentLoop: Int, unlockedSecrets: Set<String>): List<SimpleStoryNode> {
|
||||
return storyNodes.values.filter { node ->
|
||||
when {
|
||||
currentLoop < 3 -> node.id.startsWith("side_") && node.id.contains("basic")
|
||||
currentLoop < 6 -> node.id.startsWith("side_") && !node.id.contains("advanced")
|
||||
currentLoop < 10 -> !node.id.contains("endgame")
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 故事节点映射
|
||||
private val storyNodes = mapOf(
|
||||
"first_awakening" to SimpleStoryNode(
|
||||
id = "first_awakening",
|
||||
title = "第一次觉醒",
|
||||
content = """
|
||||
你在月球基地的医疗舱中醒来,头部剧痛如同被锤击。
|
||||
|
||||
周围一片混乱,设备的警报声此起彼伏,红色的警示灯在黑暗中闪烁。
|
||||
你的记忆模糊不清,但有一种奇怪的既视感...
|
||||
仿佛这种情况你已经经历过很多次了。
|
||||
|
||||
氧气显示器显示还有6小时的供应量。
|
||||
你必须立即采取行动。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "check_oxygen",
|
||||
text = "检查氧气系统",
|
||||
nextNodeId = "oxygen_crisis",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-5", "消耗体力")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "search_medical",
|
||||
text = "搜索医疗用品",
|
||||
nextNodeId = "medical_supplies",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "10", "发现止痛药")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "contact_earth",
|
||||
text = "尝试联系地球",
|
||||
nextNodeId = "communication_failure",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-3", "轻微疲劳")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"oxygen_crisis" to SimpleStoryNode(
|
||||
id = "oxygen_crisis",
|
||||
title = "氧气危机",
|
||||
content = """
|
||||
你检查了氧气系统,发现情况比预想的更糟糕。
|
||||
|
||||
主要氧气管线有三处泄漏,备用氧气罐只剩下20%。
|
||||
按照目前的消耗速度,你最多还有4小时的生存时间。
|
||||
|
||||
突然,你想起了什么...这些损坏的位置,
|
||||
你之前似乎见过。一种不祥的预感涌上心头。
|
||||
|
||||
"又是这些地方..."你喃喃自语。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "repair_system",
|
||||
text = "尝试修复氧气系统",
|
||||
nextNodeId = "repair_attempt",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-10", "重体力劳动")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "explore_base",
|
||||
text = "探索基地寻找备用氧气",
|
||||
nextNodeId = "base_exploration",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-8", "长距离移动"),
|
||||
SimpleEffect(SimpleEffectType.LOCATION_DISCOVER, "storage_bay", "发现储藏室")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "memory_fragment",
|
||||
text = "仔细回忆这种既视感",
|
||||
nextNodeId = "memory_recall",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "-5", "精神压力"),
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "time_loop_hint", "时间循环线索")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"medical_supplies" to SimpleStoryNode(
|
||||
id = "medical_supplies",
|
||||
title = "医疗补给",
|
||||
content = """
|
||||
你在医疗柜中找到了一些止痛药和绷带。
|
||||
|
||||
服用止痛药后,头痛稍有缓解,思维也清晰了一些。
|
||||
但是,当你看到医疗记录时,发现了令人不安的事实...
|
||||
|
||||
这里有你的医疗记录,但日期显示是"第27次循环"。
|
||||
什么是"循环"?你从来没有听说过这个概念。
|
||||
|
||||
在记录的末尾,你看到一行手写的字迹:
|
||||
"必须记住EVA的位置...时间锚在那里。"
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "read_records",
|
||||
text = "仔细阅读所有医疗记录",
|
||||
nextNodeId = "medical_records_detail",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "eva_location", "EVA位置线索")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "ignore_records",
|
||||
text = "忽略记录,专注当前状况",
|
||||
nextNodeId = "oxygen_crisis",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "5", "避免精神负担")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "search_eva",
|
||||
text = "立即寻找EVA",
|
||||
nextNodeId = "eva_search",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-7", "紧急搜索")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"communication_failure" to SimpleStoryNode(
|
||||
id = "communication_failure",
|
||||
title = "通讯中断",
|
||||
content = """
|
||||
你尝试联系地球,但通讯系统完全没有反应。
|
||||
|
||||
不仅如此,你发现通讯日志中最后一条记录是28小时前,
|
||||
内容是:"第27次循环开始,时间锚定失效,正在尝试修复..."
|
||||
|
||||
这条记录的发送者署名是...你自己的名字。
|
||||
但你完全不记得发送过这条信息。
|
||||
|
||||
更令人震惊的是,在这条记录之前,还有26条类似的记录,
|
||||
每一条都标注着不同的循环次数。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "check_logs",
|
||||
text = "查看所有通讯日志",
|
||||
nextNodeId = "time_loop_discovery",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "time_loop_truth", "时间循环真相")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "repair_comm",
|
||||
text = "尝试修复通讯设备",
|
||||
nextNodeId = "repair_attempt",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-7", "技术工作")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "panic_reaction",
|
||||
text = "这不可能...我在做梦",
|
||||
nextNodeId = "denial_phase",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "-10", "精神冲击")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"time_loop_discovery" to SimpleStoryNode(
|
||||
id = "time_loop_discovery",
|
||||
title = "时间循环的真相",
|
||||
content = """
|
||||
通讯日志揭示了令人震惊的真相...
|
||||
|
||||
你已经经历了27次相同的28小时循环。
|
||||
每次你都会在医疗舱中醒来,每次都会面临氧气危机,
|
||||
每次最终都会因为各种原因死亡,然后重新开始。
|
||||
|
||||
但这一次,似乎有什么不同了。
|
||||
你保留了一些记忆片段,能够意识到循环的存在。
|
||||
|
||||
在日志的最后,你看到了一条AI系统的留言:
|
||||
"主人,第28次循环已开始。时间锚定器需要手动重置。
|
||||
EVA在月球表面的坐标:月海-7, 地标-Alpha。
|
||||
警告:灾难将在28小时后发生。"
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "find_eva",
|
||||
text = "立即寻找EVA区域",
|
||||
nextNodeId = "eva_preparation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.LOCATION_DISCOVER, "eva_bay", "发现EVA舱"),
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "disaster_warning", "灾难警告")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "find_ai",
|
||||
text = "寻找AI系统获得更多信息",
|
||||
nextNodeId = "ai_encounter",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "ai_assistant", "AI助手")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "prepare_survival",
|
||||
text = "准备生存用品",
|
||||
nextNodeId = "survival_preparation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "15", "医疗用品"),
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "10", "营养补充")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"eva_preparation" to SimpleStoryNode(
|
||||
id = "eva_preparation",
|
||||
title = "EVA准备",
|
||||
content = """
|
||||
你找到了EVA(舱外活动)装备区域。
|
||||
|
||||
这里的装备看起来已经准备就绪,仿佛之前的"你"已经做过准备。
|
||||
在EVA头盔内侧,你发现了一张纸条:
|
||||
|
||||
"如果你看到这个,说明你已经开始记住了。
|
||||
时间锚在月球表面的古老遗迹中。
|
||||
但要小心,那里有东西在守护着它。
|
||||
记住:不要相信第一印象,真相藏在第三层。"
|
||||
|
||||
你的手在颤抖...这是你自己的笔迹。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "eva_mission",
|
||||
text = "穿上EVA装备,前往月球表面",
|
||||
nextNodeId = "lunar_surface",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "-15", "EVA任务"),
|
||||
SimpleEffect(SimpleEffectType.LOCATION_DISCOVER, "lunar_ruins", "月球遗迹")
|
||||
),
|
||||
requirements = listOf(
|
||||
SimpleRequirement(SimpleRequirementType.MIN_STAMINA, "20", "需要足够体力")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "study_equipment",
|
||||
text = "仔细研究EVA装备和资料",
|
||||
nextNodeId = "equipment_analysis",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "eva_knowledge", "EVA技术知识")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "rest_prepare",
|
||||
text = "先休息恢复体力",
|
||||
nextNodeId = "rest_period",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "20", "充分休息"),
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "10", "体力恢复")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
"ai_encounter" to SimpleStoryNode(
|
||||
id = "ai_encounter",
|
||||
title = "AI助手",
|
||||
content = """
|
||||
你找到了基地的AI核心系统。
|
||||
|
||||
"欢迎回来,艾丽卡博士。这是您的第28次尝试。"
|
||||
一个温和的女性声音响起。
|
||||
|
||||
"我是ARIA,您的个人AI助手。很遗憾,前27次循环都以失败告终。
|
||||
但这次有所不同...您保留了部分记忆。这是突破的希望。"
|
||||
|
||||
"时间锚位于月球古遗迹深处。那里的实体会测试您的决心。
|
||||
您必须做出三个关键选择,每个选择都会影响最终结果。
|
||||
|
||||
记住:牺牲、信任、真相。这三个词是关键。"
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "ask_disaster",
|
||||
text = "询问即将发生的灾难",
|
||||
nextNodeId = "disaster_explanation",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "disaster_truth", "灾难真相")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "ask_previous_loops",
|
||||
text = "了解前27次循环的经历",
|
||||
nextNodeId = "loop_history",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "loop_memories", "循环记忆")
|
||||
)
|
||||
),
|
||||
SimpleChoice(
|
||||
id = "request_ai_help",
|
||||
text = "请求AI协助生成策略",
|
||||
nextNodeId = "ai_strategy",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.SECRET_UNLOCK, "ai_strategy", "AI策略支持")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// 添加更多节点...
|
||||
"game_over_failure" to SimpleStoryNode(
|
||||
id = "game_over_failure",
|
||||
title = "循环重置",
|
||||
content = """
|
||||
一切都消失在白光中...
|
||||
|
||||
当你再次睁开眼睛时,你又回到了医疗舱。
|
||||
但这次,你记得更多了。
|
||||
|
||||
第29次循环开始。
|
||||
""".trimIndent(),
|
||||
choices = listOf(
|
||||
SimpleChoice(
|
||||
id = "restart_with_memory",
|
||||
text = "带着记忆重新开始",
|
||||
nextNodeId = "first_awakening",
|
||||
effects = listOf(
|
||||
SimpleEffect(SimpleEffectType.LOOP_CHANGE, "1", "新循环开始"),
|
||||
SimpleEffect(SimpleEffectType.HEALTH_CHANGE, "100", "完全恢复"),
|
||||
SimpleEffect(SimpleEffectType.STAMINA_CHANGE, "50", "体力恢复")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
11
app/src/main/java/com/example/gameofmoon/ui/theme/Color.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.example.gameofmoon.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
58
app/src/main/java/com/example/gameofmoon/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
package com.example.gameofmoon.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GameofMoonTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/com/example/gameofmoon/ui/theme/Type.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.example.gameofmoon.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
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>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
app/src/main/res/raw/ambient_mystery.mp3
Normal file
BIN
app/src/main/res/raw/button_click.mp3
Normal file
BIN
app/src/main/res/raw/discovery_chime.mp3
Normal file
BIN
app/src/main/res/raw/electronic_tension.mp3
Normal file
BIN
app/src/main/res/raw/epic_finale.mp3
Normal file
BIN
app/src/main/res/raw/error_alert.mp3
Normal file
BIN
app/src/main/res/raw/heart_monitor.mp3
Normal file
BIN
app/src/main/res/raw/notification_beep.mp3
Normal file
BIN
app/src/main/res/raw/orchestral_revelation.mp3
Normal file
BIN
app/src/main/res/raw/oxygen_leak_alert.mp3
Normal file
BIN
app/src/main/res/raw/rain_light.mp3
Normal file
BIN
app/src/main/res/raw/reactor_hum.mp3
Normal file
29
app/src/main/res/raw/readme_audio.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
音频文件下载说明
|
||||
=================
|
||||
|
||||
本目录包含游戏所需的 18 个音频文件。
|
||||
|
||||
当前状态:
|
||||
- ✅ 部分文件可能已通过脚本自动下载
|
||||
- 📄 其他文件为占位符,需要手动下载替换
|
||||
|
||||
手动下载步骤:
|
||||
1. 访问 https://pixabay.com/sound-effects/
|
||||
2. 搜索对应的音效类型 (例如: "button click", "ambient space")
|
||||
3. 下载 MP3 格式的音频文件
|
||||
4. 重命名为对应的文件名 (如 button_click.mp3)
|
||||
5. 替换本目录中的占位符文件
|
||||
|
||||
自动化工具:
|
||||
- 运行 ../../../audio_rename.sh 自动重命名下载的文件
|
||||
- 查看 ../../../AUDIO_DOWNLOAD_GUIDE.md 获取详细下载指南
|
||||
|
||||
测试音频系统:
|
||||
即使使用占位文件,游戏的音频系统也能正常运行,
|
||||
这样你就可以先测试功能,稍后再添加真实音频。
|
||||
|
||||
编译游戏:
|
||||
cd ../../../
|
||||
./gradlew assembleDebug
|
||||
|
||||
下载完成后,游戏将拥有完整的音频体验!
|
||||
BIN
app/src/main/res/raw/solar_storm.mp3
Normal file
BIN
app/src/main/res/raw/space_silence.mp3
Normal file
BIN
app/src/main/res/raw/storm_cyber.mp3
Normal file
BIN
app/src/main/res/raw/time_distortion.mp3
Normal file
BIN
app/src/main/res/raw/ventilation_soft.mp3
Normal file
BIN
app/src/main/res/raw/wind_gentle.mp3
Normal file
6
app/src/main/res/values/api_keys.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Gemini API配置 -->
|
||||
<string name="gemini_api_key">AIzaSyAO7glJMBH5BiJhqYBAOD7FTgv4tVi2HLE</string>
|
||||
<string name="gemini_api_base_url">https://generativelanguage.googleapis.com/v1beta/</string>
|
||||
</resources>
|
||||
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">GameofMoon</string>
|
||||
</resources>
|
||||
5
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.GameofMoon" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
17
app/src/test/java/com/example/gameofmoon/ExampleUnitTest.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.gameofmoon
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||