首次提交: 时间囚笼游戏完整版本

- 实现了完整的Android游戏框架 (Kotlin + Jetpack Compose)
- 科技暗黑风格UI设计与终端风格界面组件
- 完整的故事系统 (主线+支线剧情)
- 固定底部操作区布局,解决选择按钮可见性问题
- 集成Gemini AI智能对话支持
- 游戏状态管理与存档系统
- 动态天气系统与角色状态跟踪
- 支持离线游戏,兼容Android 11+
This commit is contained in:
2025-08-22 10:07:03 -07:00
commit 514ed09825
111 changed files with 10753 additions and 0 deletions

View File

@@ -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)
}
}

View 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>

View 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()
}
}
}

View 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
)

View File

@@ -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
)

View 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
}

View File

@@ -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
)
)
)
)
}
}
}

View File

@@ -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)
}
}
}
}
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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", "隐藏的悲伤")
)
)
)
)
)
}

View 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", "体力恢复")
)
)
)
)
)
}

View 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)

View 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
)
}

View 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
)
*/
)

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
下载完成后,游戏将拥有完整的音频体验!

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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>

View 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>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">GameofMoon</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.GameofMoon" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View 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>

View 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>

View 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)
}
}