2025-12-02 12:19:49 +08:00

431 lines
14 KiB
Kotlin

package com.example.pinball
import android.app.Activity
import android.content.Context
import android.graphics.*
import android.os.Bundle
import android.view.Choreographer
import android.view.MotionEvent
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import java.util.*
import kotlin.math.abs
class MainActivity : Activity() {
// Screen dimensions
private var tableWidth = 0
private var tableHeight = 0
// Paddle properties
private var racketY = 0f
private var racketHeight = 0
private var racketWidth = 0
private var racketX = 0f
// Ball properties
private var ballSize = 0
private var ballX = 0f
private var ballY = 0f
// Physics (Pixels per second)
private var velocityX = 0f
private var velocityY = 0f
private val baseSpeed = 1000f // Base speed in pixels/sec
private var lastFrameTimeNanos = 0L
// Game State
private var isLose = false
private var score = 0
private var gameTimeSeconds = 0f
private var currentLevel = 1
// Bricks
data class Brick(
val rect: RectF,
var isBroken: Boolean = false,
val color: Int
)
private var bricks = mutableListOf<Brick>()
private lateinit var gameView: GameView
private lateinit var scoreText: TextView
private lateinit var btnRestart: Button
private val rand = Random()
// Game Loop
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (isLose) return
if (lastFrameTimeNanos != 0L) {
val dt = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000_000f
updateGame(dt)
}
lastFrameTimeNanos = frameTimeNanos
gameView.invalidate()
scoreText.text = "Score: $score Level: $currentLevel"
Choreographer.getInstance().postFrameCallback(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
setContentView(R.layout.activity_main)
// Initialize dimensions
racketHeight = resources.getDimensionPixelSize(R.dimen.racket_height)
racketWidth = resources.getDimensionPixelSize(R.dimen.racket_width)
ballSize = resources.getDimensionPixelSize(R.dimen.ball_size)
val displayMetrics = resources.displayMetrics
tableWidth = displayMetrics.widthPixels
tableHeight = displayMetrics.heightPixels
// Move paddle up (approx 20% from bottom)
racketY = tableHeight * 0.80f
gameView = GameView(this)
findViewById<FrameLayout>(R.id.game_container).addView(gameView)
scoreText = findViewById(R.id.score_text)
btnRestart = findViewById(R.id.btn_restart)
val btnSave = findViewById<Button>(R.id.btn_save)
val btnLoad = findViewById<Button>(R.id.btn_load)
btnSave.setOnClickListener { saveGame() }
btnLoad.setOnClickListener { loadGame() }
btnRestart.setOnClickListener { restartGame() }
gameView.setOnTouchListener { _, event ->
handleTouch(event)
true
}
startGame()
}
private fun handleTouch(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
// Direct control: paddle follows finger X
if (event.y > tableHeight * 0.6) {
racketX = event.x - racketWidth / 2
} else {
// Fallback to side taps if touching upper area
if (event.x < tableWidth / 2) {
racketX -= 20
} else {
racketX += 20
}
}
// Clamp
if (racketX < 0) racketX = 0f
if (racketX > tableWidth - racketWidth) racketX = (tableWidth - racketWidth).toFloat()
}
}
}
private fun startGame() {
isLose = false
score = 0
gameTimeSeconds = 0f
currentLevel = 1
initLevel(currentLevel)
resetBall()
lastFrameTimeNanos = 0L
btnRestart.visibility = View.GONE
Choreographer.getInstance().removeFrameCallback(frameCallback)
Choreographer.getInstance().postFrameCallback(frameCallback)
}
private fun resetBall() {
ballX = (tableWidth / 2 - ballSize / 2).toFloat()
ballY = racketY - ballSize - 50f
racketX = (tableWidth / 2 - racketWidth / 2).toFloat()
// Reset velocity
val xyRate = rand.nextDouble() - 0.5
velocityY = -baseSpeed // Start moving up
velocityX = (baseSpeed * xyRate * 1.5).toFloat()
}
private fun initLevel(level: Int) {
bricks.clear()
val cols = 8
val brickPadding = 10f
val brickW = (tableWidth - (cols + 1) * brickPadding) / cols
val brickH = tableHeight * 0.03f
val startY = tableHeight * 0.1f
val rows = when(level) {
1 -> 4
2 -> 6
else -> 8
}
for (row in 0 until rows) {
for (col in 0 until cols) {
// Level 2+ pattern: skip some bricks
if (level > 1 && (row + col) % 2 == 0 && rand.nextBoolean()) continue
val x = brickPadding + col * (brickW + brickPadding)
val y = startY + row * (brickH + brickPadding)
val color = when(row % 4) {
0 -> Color.parseColor("#EF5350") // Red
1 -> Color.parseColor("#FFA726") // Orange
2 -> Color.parseColor("#66BB6A") // Green
else -> Color.parseColor("#42A5F5") // Blue
}
bricks.add(Brick(RectF(x, y, x + brickW, y + brickH), false, color))
}
}
}
private fun updateGame(dt: Float) {
// Move ball
ballX += velocityX * dt
ballY += velocityY * dt
// Wall collisions
if (ballX <= 0) {
ballX = 0f
velocityX = -velocityX
} else if (ballX >= tableWidth - ballSize) {
ballX = (tableWidth - ballSize).toFloat()
velocityX = -velocityX
}
// Top collision
if (ballY <= 0) {
ballY = 0f
velocityY = -velocityY
}
// Paddle collision
if (velocityY > 0 && ballY + ballSize >= racketY && ballY < racketY + racketHeight) {
if (ballX + ballSize >= racketX && ballX <= racketX + racketWidth) {
velocityY = -velocityY
// Add some "english" based on where it hit the paddle
val hitPoint = (ballX + ballSize / 2) - (racketX + racketWidth / 2)
velocityX += hitPoint * 5 // Adjust X velocity based on hit position
// Cap velocity
val maxSpeed = baseSpeed * 1.5f
if (abs(velocityX) > maxSpeed) velocityX = maxSpeed * Math.signum(velocityX)
ballY = racketY - ballSize - 1
}
}
// Brick collision
val ballRect = RectF(ballX, ballY, ballX + ballSize, ballY + ballSize)
var hitBrick = false
for (brick in bricks) {
if (!brick.isBroken && RectF.intersects(ballRect, brick.rect)) {
brick.isBroken = true
score += 10
hitBrick = true
// Simple bounce logic
val intersect = RectF(ballRect)
intersect.intersect(brick.rect)
if (intersect.width() > intersect.height()) {
velocityY = -velocityY
} else {
velocityX = -velocityX
}
break // Handle one brick per frame
}
}
// Check Level Clear
if (bricks.none { !it.isBroken }) {
currentLevel++
initLevel(currentLevel)
resetBall()
Toast.makeText(this, "Level $currentLevel", Toast.LENGTH_SHORT).show()
}
// Bottom collision (Game Over)
if (ballY > tableHeight) {
isLose = true
btnRestart.visibility = View.VISIBLE
Choreographer.getInstance().removeFrameCallback(frameCallback)
gameView.invalidate()
}
// Score logic: 1 point per second
val prevTime = gameTimeSeconds.toInt()
gameTimeSeconds += dt
if (gameTimeSeconds.toInt() > prevTime) {
score++
}
}
private fun saveGame() {
val prefs = getPreferences(Context.MODE_PRIVATE)
with(prefs.edit()) {
putFloat("ballX", ballX)
putFloat("ballY", ballY)
putFloat("racketX", racketX)
putFloat("velocityX", velocityX)
putFloat("velocityY", velocityY)
putInt("score", score)
putFloat("gameTimeSeconds", gameTimeSeconds)
putBoolean("isLose", isLose)
putInt("currentLevel", currentLevel)
// Save bricks state
val brickState = StringBuilder()
for (brick in bricks) {
brickState.append(if (brick.isBroken) "1" else "0")
}
putString("brickState", brickState.toString())
apply()
}
Toast.makeText(this, "Game Saved", Toast.LENGTH_SHORT).show()
}
private fun loadGame() {
val prefs = getPreferences(Context.MODE_PRIVATE)
if (!prefs.contains("ballX")) {
Toast.makeText(this, "No saved game found", Toast.LENGTH_SHORT).show()
return
}
ballX = prefs.getFloat("ballX", 0f)
ballY = prefs.getFloat("ballY", 0f)
racketX = prefs.getFloat("racketX", 0f)
velocityX = prefs.getFloat("velocityX", 0f)
velocityY = prefs.getFloat("velocityY", 0f)
score = prefs.getInt("score", 0)
gameTimeSeconds = prefs.getFloat("gameTimeSeconds", 0f)
isLose = prefs.getBoolean("isLose", false)
currentLevel = prefs.getInt("currentLevel", 1)
// Restore level and bricks
initLevel(currentLevel)
val brickState = prefs.getString("brickState", "") ?: ""
if (brickState.length == bricks.size) {
for (i in bricks.indices) {
bricks[i].isBroken = brickState[i] == '1'
}
}
if (isLose) {
btnRestart.visibility = View.VISIBLE
Choreographer.getInstance().removeFrameCallback(frameCallback)
gameView.invalidate()
} else {
btnRestart.visibility = View.GONE
lastFrameTimeNanos = 0L
Choreographer.getInstance().removeFrameCallback(frameCallback)
Choreographer.getInstance().postFrameCallback(frameCallback)
}
Toast.makeText(this, "Game Loaded", Toast.LENGTH_SHORT).show()
}
private fun restartGame() {
startGame()
}
override fun onPause() {
super.onPause()
Choreographer.getInstance().removeFrameCallback(frameCallback)
}
override fun onResume() {
super.onResume()
if (!isLose) {
lastFrameTimeNanos = 0L
Choreographer.getInstance().postFrameCallback(frameCallback)
}
}
inner class GameView(context: Context) : View(context) {
private val paint = Paint()
// Colors
private val bgColor = Color.parseColor("#1E1E1E")
private val paddleColor = Color.parseColor("#03DAC6")
private val ballColorStart = Color.parseColor("#BB86FC")
private val ballColorEnd = Color.parseColor("#6200EE")
private val gameOverColor = Color.parseColor("#CF6679")
private val ballShader = RadialGradient(
0f, 0f, ballSize.toFloat(),
ballColorStart, ballColorEnd,
Shader.TileMode.CLAMP
)
init {
paint.isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(bgColor)
if (isLose) {
paint.color = gameOverColor
paint.textSize = 100f
paint.textAlign = Paint.Align.CENTER
paint.typeface = Typeface.DEFAULT_BOLD
canvas.drawText("GAME OVER", width / 2f, height / 2f, paint)
paint.color = Color.WHITE
paint.textSize = 60f
canvas.drawText("Final Score: $score", width / 2f, height / 2f + 120, paint)
} else {
// Draw Bricks
for (brick in bricks) {
if (!brick.isBroken) {
paint.color = brick.color
paint.style = Paint.Style.FILL
canvas.drawRoundRect(brick.rect, 8f, 8f, paint)
}
}
// Draw Ball
canvas.save()
canvas.translate(ballX + ballSize/2f, ballY + ballSize/2f)
paint.shader = ballShader
canvas.drawCircle(0f, 0f, ballSize / 2f, paint)
paint.shader = null
canvas.restore()
// Draw Paddle
paint.color = paddleColor
paint.style = Paint.Style.FILL
val rect = RectF(racketX, racketY, racketX + racketWidth, racketY + racketHeight)
canvas.drawRoundRect(rect, 15f, 15f, paint)
// Draw floor line
paint.color = Color.DKGRAY
paint.strokeWidth = 5f
canvas.drawLine(0f, tableHeight.toFloat(), tableWidth.toFloat(), tableHeight.toFloat(), paint)
}
}
}
}