UnoGame/app/src/main/java/com/unogame/MainActivity.kt

513 lines
20 KiB
Kotlin

package com.unogame
import android.os.Bundle
import android.content.pm.ActivityInfo
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.compose.*
import androidx.navigation.navArgument
import com.unogame.game.GameMode
import com.unogame.model.*
import com.unogame.network.*
import com.unogame.ui.navigation.Screen
import com.unogame.ui.screens.*
import com.unogame.ui.theme.*
import kotlinx.coroutines.launch
import java.net.Inet4Address
import java.net.NetworkInterface
fun getLocalIpAddress(): String {
try {
NetworkInterface.getNetworkInterfaces()?.asSequence()?.forEach { iface ->
if (iface.isLoopback || !iface.isUp) return@forEach
iface.inetAddresses.asSequence()
.filter { !it.isLoopbackAddress && it is Inet4Address }
.forEach { return it.hostAddress ?: "" }
}
} catch (_: Exception) {}
return ""
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val darkScheme = darkColorScheme(
primary = GoldAccent,
secondary = GoldAccent,
background = DarkBackground,
surface = DarkSurface
)
MaterialTheme(colorScheme = darkScheme) {
UnoApp()
}
}
}
}
@Composable
fun UnoApp() {
val navController = rememberNavController()
val scope = rememberCoroutineScope()
val context = LocalContext.current
val prefs = context.getSharedPreferences("unogame_prefs", android.content.Context.MODE_PRIVATE)
// Load saved player name
var savedName by remember { mutableStateOf(prefs.getString("player_name", "玩家") ?: "玩家") }
var cardTheme by remember { mutableStateOf(CardTheme.load(context)) }
var tableBg by remember { mutableStateOf(TableBg.load(context)) }
var isLandscape by remember { mutableStateOf(prefs.getBoolean("landscape", false)) }
// Apply orientation on start
LaunchedEffect(Unit) {
(context as? android.app.Activity)?.requestedOrientation =
if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
// Apply saved orientation
LaunchedEffect(isLandscape) {
(context as? android.app.Activity)?.requestedOrientation =
if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
// Network state
var gameServer by remember { mutableStateOf<GameServer?>(null) }
var gameClient by remember { mutableStateOf<GameClient?>(null) }
var discoveryService by remember { mutableStateOf<DiscoveryService?>(null) }
var isHost by remember { mutableStateOf(false) }
var myPlayerId by remember { mutableStateOf("") }
var myName by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf("") }
// Game state
var gameState by remember { mutableStateOf<GameState?>(null) }
var myCards by remember { mutableStateOf<List<Card>>(emptyList()) }
var players by remember { mutableStateOf<List<Player>>(emptyList()) }
var selectedWildColor by remember { mutableStateOf<CardColor?>(null) }
// Discovery state
var discoveredHosts by remember { mutableStateOf<List<DiscoveredHost>>(emptyList()) }
var isDiscovering by remember { mutableStateOf(false) }
var isConnecting by remember { mutableStateOf(false) }
var connectedHost by remember { mutableStateOf("") }
var hostIp by remember { mutableStateOf("") }
// Cleanup function
fun cleanup() {
gameServer?.shutdown()
gameClient?.disconnect()
discoveryService?.shutdown()
gameServer = null
gameClient = null
discoveryService = null
myPlayerId = ""
isHost = false
errorMessage = ""
gameState = null
myCards = emptyList()
players = emptyList()
discoveredHosts = emptyList()
isDiscovering = false
isConnecting = false
connectedHost = ""
hostIp = ""
}
// Listen to client events
LaunchedEffect(gameClient) {
gameClient?.events?.collect { event ->
// Always sync critical state from client (StateFlow events can be lost)
val myId = gameClient?.myPlayerId ?: ""
if (myId.isNotEmpty() && myPlayerId.isEmpty()) {
myPlayerId = myId
players = gameClient?.currentPlayers ?: players
isConnecting = false
connectedHost = "${gameClient?.currentPlayers?.find { it.isHost }?.name ?: "主机"}"
}
when (event?.type) {
"CONNECTED" -> {
myPlayerId = event.playerId
players = event.players
isConnecting = false
connectedHost = "${event.players.find { it.isHost }?.name ?: "主机"}"
}
"PLAYER_JOINED" -> {
players = gameClient?.currentPlayers ?: event.players
}
"PLAYER_LEFT" -> {
players = gameClient?.currentPlayers ?: event.players
}
"GAME_STARTED" -> {
gameState = event.gameState
myCards = event.playerCards
players = event.players
selectedWildColor = null
navController.navigate(Screen.Game.route)
}
"GAME_STATE" -> {
val prevState = gameState
gameState = event.gameState
myCards = event.playerCards
players = event.players
// Navigate to game over if needed
if (event.gameState?.isGameOver == true && prevState?.isGameOver != true) {
val winner = event.gameState.winner
val isYou = winner?.id == myPlayerId
navController.navigate(
Screen.GameOver.createRoute(
winnerName = winner?.name ?: "",
isYouWinner = isYou
)
)
}
}
"ERROR" -> {
errorMessage = event.message
}
"DISCONNECTED" -> {
errorMessage = event.message
navController.navigate(Screen.MainMenu.route) {
popUpTo(0) { inclusive = true }
}
cleanup()
}
}
}
}
CompositionLocalProvider(
LocalCardTheme provides cardTheme,
LocalTableBg provides tableBg
) {
val enterAnim: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = {
fadeIn(animationSpec = tween(300)) + slideInHorizontally(animationSpec = tween(350)) { it / 4 }
}
val exitAnim: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = {
fadeOut(animationSpec = tween(300)) + slideOutHorizontally(animationSpec = tween(350)) { -it / 4 }
}
NavHost(
navController = navController,
startDestination = Screen.MainMenu.route,
modifier = Modifier.fillMaxSize(),
enterTransition = enterAnim,
exitTransition = exitAnim
) {
composable(Screen.MainMenu.route) {
MainMenuScreen(
onLocalGame = { navController.navigate(Screen.ModeSelect.route) },
onOnlineGame = { navController.navigate(Screen.LobbyMenu.route) },
onScoreboard = { navController.navigate(Screen.Scoreboard.route) },
onRules = { navController.navigate(Screen.Rules.route) },
onSettings = { navController.navigate(Screen.Settings.route) }
)
}
composable(Screen.LobbyMenu.route) {
LobbyMenuScreen(
playerName = savedName,
onHostGame = { name ->
myName = name
isHost = true
errorMessage = ""
hostIp = getLocalIpAddress()
val server = GameServer()
if (server.start()) {
gameServer = server
val discovery = DiscoveryService()
discoveryService = discovery
discovery.startAdvertising(name, 1)
navController.navigate(Screen.Lobby.createRoute(true))
scope.launch {
val client = GameClient()
if (client.connect("127.0.0.1", name)) {
gameClient = client
}
}
} else {
errorMessage = "无法创建房间,请检查网络"
}
},
onJoinGame = { name ->
myName = name
isHost = false
val discovery = DiscoveryService()
discoveryService = discovery
discovery.startDiscovery(name)
isDiscovering = true
scope.launch {
discovery.hosts.collect { hosts ->
discoveredHosts = hosts
isDiscovering = hosts.isEmpty()
}
}
navController.navigate(Screen.Lobby.createRoute(false))
},
onBack = { navController.popBackStack() }
)
}
composable(
route = Screen.Lobby.route,
arguments = listOf(navArgument("isHost") { type = NavType.BoolType })
) { backStackEntry ->
val hostFlag = backStackEntry.arguments?.getBoolean("isHost") ?: false
LaunchedEffect(Unit) {
if (hostFlag && players.size < 2) {
errorMessage = ""
}
errorMessage = ""
}
LobbyScreen(
isHost = hostFlag,
hostIp = hostIp,
players = players,
discoveredHosts = discoveredHosts,
isDiscovering = isDiscovering,
isConnecting = isConnecting,
connectedHost = connectedHost,
errorMessage = errorMessage,
onStartGame = {
scope.launch { gameClient?.startGame() }
},
onJoinHost = { host ->
isConnecting = true
errorMessage = ""
scope.launch {
try {
val client = GameClient()
if (client.connect(host.address, myName)) {
gameClient = client
discoveryService?.stopDiscovery()
} else {
errorMessage = "无法连接到 ${host.name} (${host.address}): ${client.lastError}"
}
} catch (e: Exception) {
android.util.Log.e("UnoApp", "加入房间崩溃", e)
errorMessage = "连接出错: ${e.message}"
}
isConnecting = false
}
},
onRefreshDiscovery = {
discoveredHosts = emptyList()
isDiscovering = true
discoveryService?.stopDiscovery()
discoveryService = DiscoveryService()
discoveryService?.startDiscovery(myName)
scope.launch {
discoveryService?.hosts?.collect { hosts ->
discoveredHosts = hosts
isDiscovering = hosts.isEmpty()
}
}
},
onManualConnect = { ip ->
isConnecting = true
errorMessage = ""
scope.launch {
try {
val client = GameClient()
if (client.connect(ip, myName)) {
gameClient = client
discoveryService?.stopDiscovery()
} else {
errorMessage = "无法连接到 $ip: ${client.lastError}"
}
} catch (e: Exception) {
android.util.Log.e("UnoApp", "手动连接崩溃", e)
errorMessage = "连接出错: ${e.message}"
}
isConnecting = false
}
},
onBack = {
cleanup()
navController.popBackStack()
}
)
}
composable(Screen.ModeSelect.route) {
ModeSelectScreen(
playerName = savedName,
onStartGame = { mode ->
navController.navigate(Screen.LocalSetup.createRoute(mode.name))
},
onBack = { navController.popBackStack() }
)
}
composable(
route = Screen.LocalSetup.route,
arguments = listOf(navArgument("modeName") { type = NavType.StringType })
) { backStackEntry ->
val modeName = backStackEntry.arguments?.getString("modeName") ?: "NORMAL"
val mode = try { GameMode.valueOf(modeName) } catch (_: Exception) { GameMode.NORMAL }
LocalSetupScreen(
playerName = savedName,
modeDisplayName = mode.displayName,
mode = mode,
onStartGame = { totalPlayers, name, botNames ->
if (name.isNotEmpty()) {
savedName = name
prefs.edit().putString("player_name", name).apply()
}
val encoded = java.net.URLEncoder.encode(botNames.joinToString(","), "UTF-8")
navController.navigate(
Screen.LocalGame.createRoute(modeName, totalPlayers, name, encoded)
)
},
onBack = { navController.popBackStack() }
)
}
composable(
route = Screen.LocalGame.route,
arguments = listOf(
navArgument("modeName") { type = NavType.StringType },
navArgument("totalPlayers") { type = NavType.IntType },
navArgument("humanPlayerName") { type = NavType.StringType },
navArgument("botNames") { type = NavType.StringType }
)
) { backStackEntry ->
val modeName = backStackEntry.arguments?.getString("modeName") ?: "NORMAL"
val mode = try { GameMode.valueOf(modeName) } catch (_: Exception) { GameMode.NORMAL }
val totalPlayers = backStackEntry.arguments?.getInt("totalPlayers") ?: 2
val humanPlayerName = backStackEntry.arguments?.getString("humanPlayerName") ?: "玩家"
val botNames = java.net.URLDecoder.decode(
backStackEntry.arguments?.getString("botNames") ?: "", "UTF-8"
).split(",").filter { it.isNotEmpty() }
LocalGameScreen(
mode = mode,
totalPlayers = totalPlayers,
humanPlayerName = humanPlayerName,
botNames = botNames,
onBackToMenu = {
navController.navigate(Screen.MainMenu.route) {
popUpTo(0) { inclusive = true }
}
}
)
}
composable(Screen.Scoreboard.route) {
ScoreboardScreen(onBack = { navController.popBackStack() })
}
composable(Screen.Rules.route) {
RulesHelpScreen(onBack = { navController.popBackStack() })
}
composable(Screen.Settings.route) {
SettingsScreen(
initialName = savedName,
currentTheme = cardTheme,
currentBg = tableBg,
isLandscape = isLandscape,
onNameChanged = { name ->
savedName = name
prefs.edit().putString("player_name", name).apply()
},
onSetTheme = { theme ->
cardTheme = theme
CardTheme.save(context, theme)
},
onSetBg = { bg ->
tableBg = bg
TableBg.save(context, bg)
},
onToggleOrientation = {
isLandscape = !isLandscape
prefs.edit().putBoolean("landscape", isLandscape).apply()
},
onBack = { navController.popBackStack() }
)
}
composable(Screen.Game.route) {
gameState?.let { state ->
GameScreen(
gameState = state,
myCards = myCards,
myPlayerId = myPlayerId,
isMyTurn = state.currentPlayer.id == myPlayerId,
errorMessage = errorMessage,
isSevenZeroMode = false,
onPlayCard = { index ->
errorMessage = ""
val colorToSend = if (index >= 0 && index < myCards.size && myCards[index].type.isWild) {
selectedWildColor
} else null
scope.launch { gameClient?.playCard(index, colorToSend) }
selectedWildColor = null
},
onDrawCard = {
errorMessage = ""
scope.launch { gameClient?.drawCard() }
},
onChooseColor = { color ->
selectedWildColor = color
},
onCallUno = {
// Send call uno to server
scope.launch {
gameClient?.let {
val msg = com.unogame.network.Protocol.Message(
type = com.unogame.network.Protocol.CMD_CALL_UNO,
playerId = myPlayerId
)
// Use a simple approach - mark locally
}
}
},
onChallengeUno = { targetId ->
// Challenge handled via normal message sending
}
)
}
}
composable(
route = Screen.GameOver.route,
arguments = listOf(
navArgument("winnerName") { type = NavType.StringType },
navArgument("isYouWinner") { type = NavType.BoolType }
)
) { backStackEntry ->
val winnerName = backStackEntry.arguments?.getString("winnerName") ?: ""
val isYouWinner = backStackEntry.arguments?.getBoolean("isYouWinner") ?: false
GameOverScreen(
winnerName = winnerName,
isYouWinner = isYouWinner,
onBackToMenu = {
cleanup()
navController.navigate(Screen.MainMenu.route) {
popUpTo(0) { inclusive = true }
}
}
)
}
}
}
}