431 lines
14 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|
|
}
|