Important stuff
This commit is contained in:
parent
693607de7c
commit
b65222ddf9
1 changed files with 434 additions and 34 deletions
|
@ -52,16 +52,33 @@ class SecretView @JvmOverloads constructor(
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
textSize = 48f
|
textSize = 48f
|
||||||
}
|
}
|
||||||
|
private var colorSecondary: Int = Color.GREEN
|
||||||
|
|
||||||
private var playerX = 0f
|
private var playerX = 0f
|
||||||
private var viewWidth = 0f
|
private var viewWidth = 0f
|
||||||
private var viewHeight = 0f
|
private var viewHeight = 0f
|
||||||
|
|
||||||
|
private var enemyClearAggression = 0f
|
||||||
|
private var adaptiveSpawnTimer = 0L
|
||||||
|
private var lastEnemyCount = 0
|
||||||
|
private var avgEnemiesPerSecond = 0f
|
||||||
|
|
||||||
private var isTouching = false
|
private var isTouching = false
|
||||||
|
private var shieldRechargeTimer = 0L
|
||||||
|
private var shieldFlashAlpha = 0f
|
||||||
|
private var lastMissileSide = -1
|
||||||
|
private var missileCooldown = 0L
|
||||||
private var bulletCooldownMs = 0L
|
private var bulletCooldownMs = 0L
|
||||||
private var gameOver = false
|
private var gameOver = false
|
||||||
private var score = 0
|
private var score = 0
|
||||||
|
|
||||||
|
// Player level
|
||||||
|
private var multiFireLevel = 1
|
||||||
|
private var piercingLevel = 1
|
||||||
|
private var shieldLevel = 0
|
||||||
|
private var missileLevel = 0
|
||||||
|
private var rapidFireLevel = 1
|
||||||
|
|
||||||
private val bullets = mutableListOf<Bullet>()
|
private val bullets = mutableListOf<Bullet>()
|
||||||
private val enemyBullets = mutableListOf<Bullet>()
|
private val enemyBullets = mutableListOf<Bullet>()
|
||||||
private val enemies = mutableListOf<Enemy>()
|
private val enemies = mutableListOf<Enemy>()
|
||||||
|
@ -69,11 +86,29 @@ class SecretView @JvmOverloads constructor(
|
||||||
private val stars = mutableListOf<Star>()
|
private val stars = mutableListOf<Star>()
|
||||||
private val explosions = mutableListOf<Explosion>()
|
private val explosions = mutableListOf<Explosion>()
|
||||||
private val rocketTrails = mutableListOf<Pair<Float, Float>>()
|
private val rocketTrails = mutableListOf<Pair<Float, Float>>()
|
||||||
|
private val pickups = mutableListOf<Pickup>()
|
||||||
|
private val playerMissiles = mutableListOf<PlayerMissile>()
|
||||||
|
|
||||||
private data class Bullet(var x: Float, var y: Float, val dy: Float = -15f)
|
private data class Bullet(var x: Float, var y: Float, val dy: Float = -15f, var life: Int = 1)
|
||||||
private data class Star(var x: Float, var y: Float, val radius: Float, val speed: Float)
|
private data class Star(var x: Float, var y: Float, val radius: Float, val speed: Float)
|
||||||
private data class Rocket(var x: Float, var y: Float, var angle: Float, val trail: MutableList<Pair<Float, Float>> = mutableListOf())
|
private data class Rocket(var x: Float, var y: Float, var angle: Float, val trail: MutableList<Pair<Float, Float>> = mutableListOf())
|
||||||
private data class Explosion(var x: Float, var y: Float, var timer: Int = 12)
|
private data class Explosion(var x: Float, var y: Float, var timer: Int = 12)
|
||||||
|
private data class Pickup(
|
||||||
|
val x: Float,
|
||||||
|
var y: Float,
|
||||||
|
val type: Int,
|
||||||
|
var hue: Float = Random.nextFloat() * 360f,
|
||||||
|
)
|
||||||
|
private data class PlayerMissile(
|
||||||
|
var x: Float,
|
||||||
|
var y: Float,
|
||||||
|
var angle: Float,
|
||||||
|
var ttl: Long = 90000L,
|
||||||
|
var target: Enemy? = null,
|
||||||
|
var recheckCooldown: Long = 0L,
|
||||||
|
var side: Int, // -1 = left, 1 = right
|
||||||
|
val trail: MutableList<Pair<Float, Float>> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
private var lastLogicTime = 0L
|
private var lastLogicTime = 0L
|
||||||
private val logicStepMs = 16L
|
private val logicStepMs = 16L
|
||||||
|
@ -88,11 +123,14 @@ class SecretView @JvmOverloads constructor(
|
||||||
stars.add(Star(Random.nextFloat() * 1080f, Random.nextFloat() * 1920f, Random.nextFloat() * 2f + 1f, Random.nextFloat() * 2f + 0.5f))
|
stars.add(Star(Random.nextFloat() * 1080f, Random.nextFloat() * 1920f, Random.nextFloat() * 2f + 1f, Random.nextFloat() * 2f + 0.5f))
|
||||||
}
|
}
|
||||||
|
|
||||||
val colorAttrs = intArrayOf(com.google.android.material.R.attr.colorPrimaryVariant)
|
val colorAttrs = intArrayOf(
|
||||||
|
com.google.android.material.R.attr.colorPrimaryVariant,
|
||||||
|
com.google.android.material.R.attr.colorSecondary
|
||||||
|
)
|
||||||
context.obtainStyledAttributes(colorAttrs).use {
|
context.obtainStyledAttributes(colorAttrs).use {
|
||||||
val primary = it.getColor(0, Color.CYAN)
|
playerPaint.color = it.getColor(0, Color.CYAN)
|
||||||
playerPaint.color = primary
|
enemyPaint.color = it.getColor(0, Color.CYAN)
|
||||||
enemyPaint.color = primary
|
colorSecondary = it.getColor(1, Color.GREEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
Choreographer.getInstance().postFrameCallback(this)
|
Choreographer.getInstance().postFrameCallback(this)
|
||||||
|
@ -129,6 +167,20 @@ class SecretView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update shield recharge timer and flash animation
|
||||||
|
if (shieldRechargeTimer > 0) {
|
||||||
|
shieldRechargeTimer -= deltaMs
|
||||||
|
if (shieldRechargeTimer <= 0) {
|
||||||
|
shieldRechargeTimer = 0
|
||||||
|
shieldFlashAlpha = 1f // trigger visual flash on recharge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shieldFlashAlpha > 0f) {
|
||||||
|
shieldFlashAlpha -= deltaMs / 300f
|
||||||
|
if (shieldFlashAlpha < 0f) shieldFlashAlpha = 0f
|
||||||
|
}
|
||||||
|
|
||||||
rockets.forEach { rocket ->
|
rockets.forEach { rocket ->
|
||||||
rocket.trail.add(0, rocket.x to rocket.y)
|
rocket.trail.add(0, rocket.x to rocket.y)
|
||||||
if (rocket.trail.size > 20) {
|
if (rocket.trail.size > 20) {
|
||||||
|
@ -139,14 +191,27 @@ class SecretView @JvmOverloads constructor(
|
||||||
explosions.forEach { it.timer-- }
|
explosions.forEach { it.timer-- }
|
||||||
explosions.removeIf { it.timer <= 0 }
|
explosions.removeIf { it.timer <= 0 }
|
||||||
|
|
||||||
|
|
||||||
bullets.forEach { it.y += it.dy * deltaMs / 16f }
|
bullets.forEach { it.y += it.dy * deltaMs / 16f }
|
||||||
bullets.removeIf { it.y < 0 }
|
bullets.removeIf { it.y < 0 || it.life <= 0 }
|
||||||
|
|
||||||
bulletCooldownMs -= deltaMs
|
bulletCooldownMs -= deltaMs
|
||||||
if (isTouching && bulletCooldownMs <= 0) {
|
if (isTouching && bulletCooldownMs <= 0) {
|
||||||
bullets.add(Bullet(playerX, viewHeight - 130f))
|
val baseCooldown = 450f
|
||||||
bulletCooldownMs = 150L
|
var multiplier = 1f
|
||||||
|
for (level in 2..rapidFireLevel) {
|
||||||
|
val reduction = ((36 - level * 1.2f).coerceAtLeast(4f)) / 100f
|
||||||
|
multiplier *= (1f - reduction)
|
||||||
|
}
|
||||||
|
bulletCooldownMs = max(50L, (baseCooldown * multiplier).toLong())
|
||||||
|
|
||||||
|
val baseY = viewHeight - 100f
|
||||||
|
val spacing = 10f
|
||||||
|
val count = multiFireLevel
|
||||||
|
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val offset = (i - (count - 1) / 2f) * spacing
|
||||||
|
bullets.add(Bullet(playerX + offset, baseY))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enemies.forEach {
|
enemies.forEach {
|
||||||
|
@ -155,6 +220,12 @@ class SecretView @JvmOverloads constructor(
|
||||||
if (it.y > viewHeight + 100f) it.y = -40f // respawn at top
|
if (it.y > viewHeight + 100f) it.y = -40f // respawn at top
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pickups.forEach {
|
||||||
|
it.y += 2.5f * deltaMs / 16f
|
||||||
|
it.hue = (it.hue + deltaMs * 0.01f) % 360f
|
||||||
|
}
|
||||||
|
pickups.removeIf { it.y > viewHeight - 40f }
|
||||||
|
|
||||||
rockets.forEach {
|
rockets.forEach {
|
||||||
val targetAngle = atan2(viewHeight - 100f - it.y, playerX - it.x)
|
val targetAngle = atan2(viewHeight - 100f - it.y, playerX - it.x)
|
||||||
it.angle += ((targetAngle - it.angle + PI).mod(2 * PI) - PI).toFloat() * 0.1f
|
it.angle += ((targetAngle - it.angle + PI).mod(2 * PI) - PI).toFloat() * 0.1f
|
||||||
|
@ -166,58 +237,237 @@ class SecretView @JvmOverloads constructor(
|
||||||
enemyBullets.forEach { it.y += it.dy * deltaMs / 16f }
|
enemyBullets.forEach { it.y += it.dy * deltaMs / 16f }
|
||||||
enemyBullets.removeIf { it.y > viewHeight }
|
enemyBullets.removeIf { it.y > viewHeight }
|
||||||
|
|
||||||
|
updatePlayerMissiles(deltaMs)
|
||||||
|
|
||||||
checkCollisions()
|
checkCollisions()
|
||||||
spawnEnemies(deltaMs)
|
spawnEnemies(deltaMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkCollisions() {
|
private fun checkCollisions() {
|
||||||
|
val pickupIter = pickups.iterator()
|
||||||
|
while (pickupIter.hasNext()) {
|
||||||
|
val p = pickupIter.next()
|
||||||
|
if (hypot(playerX - p.x, viewHeight - 100f - p.y) < 40f) {
|
||||||
|
when (p.type) {
|
||||||
|
0 -> { // Multi-fire
|
||||||
|
if (multiFireLevel < 4) {
|
||||||
|
multiFireLevel++
|
||||||
|
} else {
|
||||||
|
applyRandomPowerupExcept(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 -> shieldLevel++
|
||||||
|
2 -> if (missileLevel < 8) missileLevel++
|
||||||
|
3 -> if (rapidFireLevel < 30) rapidFireLevel++
|
||||||
|
4 -> if (piercingLevel < 15) piercingLevel++
|
||||||
|
}
|
||||||
|
pickupIter.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val enemyIter = enemies.iterator()
|
val enemyIter = enemies.iterator()
|
||||||
while (enemyIter.hasNext()) {
|
while (enemyIter.hasNext()) {
|
||||||
val enemy = enemyIter.next()
|
val enemy = enemyIter.next()
|
||||||
if (hypot(playerX - enemy.x, viewHeight - 100f - enemy.y) < 40f) {
|
if (hypot(playerX - enemy.x, viewHeight - 100f - enemy.y) < 40f) {
|
||||||
explosions.add(Explosion(playerX, viewHeight - 100f))
|
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||||
gameOver = true
|
shieldRechargeTimer = calculateShieldRechargeTime()
|
||||||
return
|
enemyIter.remove()
|
||||||
|
explosions.add(Explosion(enemy.x, enemy.y))
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
explosions.add(Explosion(playerX, viewHeight - 100f))
|
||||||
|
gameOver = true
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (b in bullets) {
|
for (b in bullets) {
|
||||||
if (hypot(b.x - enemy.x, b.y - enemy.y) < 30f) {
|
if (hypot(b.x - enemy.x, b.y - enemy.y) < 30f) {
|
||||||
explosions.add(Explosion(enemy.x, enemy.y))
|
explosions.add(Explosion(enemy.x, enemy.y))
|
||||||
enemyIter.remove()
|
enemyIter.remove()
|
||||||
score += 10
|
score += 10
|
||||||
|
b.life--
|
||||||
|
|
||||||
|
if (Random.nextFloat() < 0.03f) {
|
||||||
|
val type = Random.nextInt(5) // 5 pickup types
|
||||||
|
val hue = Random.nextFloat() * 360f
|
||||||
|
pickups.add(Pickup(enemy.x, enemy.y, type, hue))
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (b in enemyBullets) {
|
val bulletIter = enemyBullets.iterator()
|
||||||
if (hypot(b.x - playerX, b.y - (viewHeight - 100f)) < 20f) {
|
while (bulletIter.hasNext()) {
|
||||||
explosions.add(Explosion(playerX, viewHeight - 100f))
|
val b = bulletIter.next()
|
||||||
gameOver = true
|
val dy = b.y - (viewHeight - 100f)
|
||||||
return
|
val dx = b.x - playerX
|
||||||
|
val dist = hypot(dx, dy)
|
||||||
|
val hitRadius = if (shieldLevel > 0 && shieldRechargeTimer <= 0) 60f else 20f
|
||||||
|
|
||||||
|
if (dist < hitRadius) {
|
||||||
|
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||||
|
shieldRechargeTimer = calculateShieldRechargeTime()
|
||||||
|
bulletIter.remove()
|
||||||
|
explosions.add(Explosion(b.x, b.y))
|
||||||
|
} else {
|
||||||
|
explosions.add(Explosion(playerX, viewHeight - 100f))
|
||||||
|
gameOver = true
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rocketIter = rockets.iterator()
|
val rocketIter = rockets.iterator()
|
||||||
while (rocketIter.hasNext()) {
|
while (rocketIter.hasNext()) {
|
||||||
val rocket = rocketIter.next()
|
val rocket = rocketIter.next()
|
||||||
if (hypot(playerX - rocket.x, viewHeight - 100f - rocket.y) < 30f) {
|
val dy = viewHeight - 100f - rocket.y
|
||||||
explosions.add(Explosion(playerX, viewHeight - 100f))
|
val dx = playerX - rocket.x
|
||||||
gameOver = true
|
val dist = hypot(dx, dy)
|
||||||
return
|
val hitRadius = if (shieldLevel > 0 && shieldRechargeTimer <= 0) 60f else 30f
|
||||||
|
|
||||||
|
if (dist < hitRadius) {
|
||||||
|
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||||
|
shieldRechargeTimer = calculateShieldRechargeTime()
|
||||||
|
rocketIter.remove()
|
||||||
|
explosions.add(Explosion(rocket.x, rocket.y))
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
explosions.add(Explosion(playerX, viewHeight - 100f))
|
||||||
|
gameOver = true
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (b in bullets) {
|
for (b in bullets) {
|
||||||
if (hypot(b.x - rocket.x, b.y - rocket.y) < 20f) {
|
if (hypot(b.x - rocket.x, b.y - rocket.y) < 20f) {
|
||||||
explosions.add(Explosion(rocket.x, rocket.y))
|
explosions.add(Explosion(rocket.x, rocket.y))
|
||||||
rocketIter.remove()
|
rocketIter.remove()
|
||||||
score += 15
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val missileIter = playerMissiles.iterator()
|
||||||
|
while (missileIter.hasNext()) {
|
||||||
|
val missile = missileIter.next()
|
||||||
|
val enemyHit = enemies.firstOrNull { enemy ->
|
||||||
|
hypot(missile.x - enemy.x, missile.y - enemy.y) < 30f
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enemyHit != null) {
|
||||||
|
explosions.add(Explosion(enemyHit.x, enemyHit.y))
|
||||||
|
enemies.remove(enemyHit)
|
||||||
|
missileIter.remove()
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
if (Random.nextFloat() < 0.5f) {
|
||||||
|
val type = Random.nextInt(5)
|
||||||
|
val hue = Random.nextFloat() * 360f
|
||||||
|
pickups.add(Pickup(enemyHit.x, enemyHit.y, type, hue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlayerMissiles(deltaMs: Long) {
|
||||||
|
// Missile cooldown and launch
|
||||||
|
missileCooldown -= deltaMs
|
||||||
|
if (missileLevel > 0 && missileCooldown <= 0) {
|
||||||
|
val cooldown = when (missileLevel) {
|
||||||
|
1 -> 20000L
|
||||||
|
2 -> 15000L
|
||||||
|
3 -> 12000L
|
||||||
|
4 -> 20000L
|
||||||
|
5 -> 18000L
|
||||||
|
6 -> 17000L
|
||||||
|
7 -> 16000L
|
||||||
|
else -> 15000L
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseY = viewHeight - 100f
|
||||||
|
if (missileLevel >= 4) {
|
||||||
|
// fire both sides
|
||||||
|
playerMissiles.add(PlayerMissile(playerX - 20f, baseY, -PI.toFloat() / 2f, side = -1))
|
||||||
|
playerMissiles.add(PlayerMissile(playerX + 20f, baseY, -PI.toFloat() / 2f, side = 1))
|
||||||
|
} else {
|
||||||
|
lastMissileSide *= -1
|
||||||
|
val offsetX = 20f * lastMissileSide
|
||||||
|
playerMissiles.add(PlayerMissile(playerX + offsetX, baseY, -PI.toFloat() / 2f, side = lastMissileSide))
|
||||||
|
}
|
||||||
|
|
||||||
|
missileCooldown = cooldown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update missiles
|
||||||
|
val lockedEnemies = playerMissiles.mapNotNull { it.target }.toSet()
|
||||||
|
val availableTargets = enemies.filter { it !in lockedEnemies }
|
||||||
|
|
||||||
|
playerMissiles.forEach { missile ->
|
||||||
|
missile.ttl -= deltaMs
|
||||||
|
if (missile.ttl <= 0) return@forEach
|
||||||
|
|
||||||
|
// Add current position to trail
|
||||||
|
missile.trail.add(0, missile.x to missile.y)
|
||||||
|
if (missile.trail.size > 20) {
|
||||||
|
missile.trail.removeLast()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missile.target == null || !enemies.contains(missile.target)) {
|
||||||
|
missile.recheckCooldown -= deltaMs
|
||||||
|
if (missile.recheckCooldown <= 0) {
|
||||||
|
val newTarget = availableTargets.minByOrNull {
|
||||||
|
hypot((it.x - missile.x).toDouble(), (it.y - missile.y).toDouble())
|
||||||
|
}
|
||||||
|
if (newTarget != null) {
|
||||||
|
missile.target = newTarget
|
||||||
|
} else {
|
||||||
|
missile.recheckCooldown = 1000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = missile.target
|
||||||
|
val angleTo = if (target != null) {
|
||||||
|
atan2(target.y - missile.y, target.x - missile.x)
|
||||||
|
} else missile.angle
|
||||||
|
|
||||||
|
// steer towards target slowly
|
||||||
|
missile.angle += ((angleTo - missile.angle + PI).mod(2 * PI) - PI).toFloat() * 0.1f
|
||||||
|
missile.x += cos(missile.angle) * 6f
|
||||||
|
missile.y += sin(missile.angle) * 6f
|
||||||
|
}
|
||||||
|
|
||||||
|
playerMissiles.removeIf { it.ttl <= 0 || it.x < 0 || it.x > viewWidth || it.y < 0 || it.y > viewHeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyRandomPowerupExcept(excludedType: Int) {
|
||||||
|
val types = (0..4).filter { it != excludedType }
|
||||||
|
val type = types.random()
|
||||||
|
when (type) {
|
||||||
|
1 -> shieldLevel++
|
||||||
|
2 -> if (missileLevel < 8) missileLevel++
|
||||||
|
3 -> if (rapidFireLevel < 30) rapidFireLevel++
|
||||||
|
4 -> if (piercingLevel < 15) piercingLevel++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCubeIconPath(): Path {
|
||||||
|
return Path().apply {
|
||||||
|
addRect(-10f, -10f, 10f, 10f, Path.Direction.CW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateShieldRechargeTime(): Long {
|
||||||
|
val base = 60000L // 60 seconds
|
||||||
|
val min = 10000L // 10 seconds
|
||||||
|
val reduction = (1.0 - exp(-shieldLevel / 40.0)).toFloat()
|
||||||
|
return (base - (base - min) * reduction).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun spawnEnemies(deltaMs: Long) {
|
private fun spawnEnemies(deltaMs: Long) {
|
||||||
waveTimer -= deltaMs
|
waveTimer -= deltaMs
|
||||||
|
adaptiveSpawnTimer += deltaMs
|
||||||
|
|
||||||
// Passive asteroids
|
// Passive asteroids
|
||||||
if (Random.nextFloat() < 0.002f) {
|
if (Random.nextFloat() < 0.002f) {
|
||||||
|
@ -233,40 +483,67 @@ class SecretView @JvmOverloads constructor(
|
||||||
else -> "hard"
|
else -> "hard"
|
||||||
}
|
}
|
||||||
|
|
||||||
enemiesLeftInWave = when (currentWaveType) {
|
val difficultyBoost = when (currentWaveType) {
|
||||||
"easy" -> 3 + currentWave
|
"easy" -> (currentWave * 1.2f).roundToInt()
|
||||||
"medium" -> 2 + currentWave / 2
|
"medium" -> (currentWave * 1.5f).roundToInt()
|
||||||
"hard" -> 1 + currentWave / 4
|
"hard" -> (currentWave * 1.8f).roundToInt()
|
||||||
else -> 3
|
else -> currentWave
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enemiesLeftInWave = 3 + difficultyBoost
|
||||||
waveTimer = 3000L
|
waveTimer = 3000L
|
||||||
|
adaptiveSpawnTimer = 0L
|
||||||
|
lastEnemyCount = enemies.size
|
||||||
|
avgEnemiesPerSecond = 0f
|
||||||
|
enemyClearAggression = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust aggression every 1s
|
||||||
|
if (adaptiveSpawnTimer >= 1000L) {
|
||||||
|
val cleared = (lastEnemyCount - enemies.size).coerceAtLeast(0)
|
||||||
|
avgEnemiesPerSecond = avgEnemiesPerSecond * 0.7f + cleared * 0.3f
|
||||||
|
lastEnemyCount = enemies.size
|
||||||
|
adaptiveSpawnTimer = 0L
|
||||||
|
|
||||||
|
// Normalize to 0–1 range (assuming 0 to 6 cleared per second)
|
||||||
|
enemyClearAggression = (avgEnemiesPerSecond / 6f).coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn enemies in group
|
// Spawn enemies in group
|
||||||
if (enemiesLeftInWave > 0 && enemies.count { it !is EnemyAsteroid } < 3) {
|
if (enemiesLeftInWave > 0 && enemies.count { it !is EnemyAsteroid } < 10) {
|
||||||
val baseX = Random.nextFloat() * (viewWidth - 100f) + 50f
|
val baseX = Random.nextFloat() * (viewWidth - 100f) + 50f
|
||||||
val baseY = -40f
|
val baseY = -40f
|
||||||
val spacing = 35f
|
val spacing = 35f
|
||||||
val sharedOffset = Random.nextFloat() * 1000f
|
val sharedOffset = Random.nextFloat() * 1000f
|
||||||
val sharedFireTime = Random.nextLong(2000L, 4000L)
|
val sharedFireTime = Random.nextLong(2000L, 4000L)
|
||||||
|
|
||||||
val formationSize = when (currentWaveType) {
|
// Dynamically adjust formation size
|
||||||
|
val formationBase = when (currentWaveType) {
|
||||||
"easy" -> 5
|
"easy" -> 5
|
||||||
"medium" -> 3
|
"medium" -> 3
|
||||||
"hard" -> 1
|
"hard" -> 1
|
||||||
else -> 3
|
else -> 3
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in 0 until min(formationSize, enemiesLeftInWave)) {
|
// Scale formation by aggression (max +2)
|
||||||
|
val dynamicBonus = (enemyClearAggression * 2).roundToInt()
|
||||||
|
val formationSize = (formationBase + dynamicBonus).coerceAtMost(enemiesLeftInWave)
|
||||||
|
|
||||||
|
for (i in 0 until formationSize) {
|
||||||
val offsetX = (i - (formationSize - 1) / 2f) * spacing
|
val offsetX = (i - (formationSize - 1) / 2f) * spacing
|
||||||
val x = baseX + offsetX
|
val x = baseX + offsetX
|
||||||
|
|
||||||
// The group sync isn't working as enemies gradually get out of sync, but whatever, it's fine.
|
|
||||||
val enemy = when (currentWaveType) {
|
val enemy = when (currentWaveType) {
|
||||||
"easy" -> EnemyEasy(x, baseY)
|
"easy" -> EnemyEasy(x, baseY)
|
||||||
"medium" -> EnemyMedium(x, baseY, sharedOffset, sharedFireTime) { enemyBullets.add(it) }
|
"medium" -> EnemyMedium(x, baseY, sharedOffset, sharedFireTime) { enemyBullets.add(it) }
|
||||||
"hard" -> EnemyHard(x, baseY, { rockets.add(it) }, sharedOffset, sharedFireTime)
|
"hard" -> {
|
||||||
|
val hardEnemiesSoFar = enemies.count { it is EnemyHard }
|
||||||
|
if (hardEnemiesSoFar >= 2) {
|
||||||
|
EnemyMedium(x, baseY, sharedOffset, sharedFireTime) { enemyBullets.add(it) }
|
||||||
|
} else {
|
||||||
|
EnemyHard(x, baseY, { rockets.add(it) }, sharedOffset, sharedFireTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> EnemyEasy(x, baseY)
|
else -> EnemyEasy(x, baseY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,10 +560,31 @@ class SecretView @JvmOverloads constructor(
|
||||||
rockets.clear()
|
rockets.clear()
|
||||||
explosions.clear()
|
explosions.clear()
|
||||||
rocketTrails.clear()
|
rocketTrails.clear()
|
||||||
|
playerMissiles.clear()
|
||||||
|
pickups.clear()
|
||||||
|
|
||||||
|
// Reset upgrades
|
||||||
|
multiFireLevel = 1
|
||||||
|
piercingLevel = 1
|
||||||
|
shieldLevel = 0
|
||||||
|
missileLevel = 0
|
||||||
|
rapidFireLevel = 1
|
||||||
|
|
||||||
|
// Reset gameplay state
|
||||||
score = 0
|
score = 0
|
||||||
currentWave = 0
|
currentWave = 0
|
||||||
enemiesLeftInWave = 0
|
enemiesLeftInWave = 0
|
||||||
gameOver = false
|
gameOver = false
|
||||||
|
shieldRechargeTimer = 0L
|
||||||
|
shieldFlashAlpha = 0f
|
||||||
|
lastMissileSide = -1
|
||||||
|
missileCooldown = 0L
|
||||||
|
bulletCooldownMs = 0L
|
||||||
|
enemyClearAggression = 0f
|
||||||
|
adaptiveSpawnTimer = 0L
|
||||||
|
lastEnemyCount = 0
|
||||||
|
avgEnemiesPerSecond = 0f
|
||||||
|
|
||||||
playerX = viewWidth / 2f
|
playerX = viewWidth / 2f
|
||||||
lastLogicTime = System.nanoTime()
|
lastLogicTime = System.nanoTime()
|
||||||
Choreographer.getInstance().postFrameCallback(this)
|
Choreographer.getInstance().postFrameCallback(this)
|
||||||
|
@ -299,8 +597,13 @@ class SecretView @JvmOverloads constructor(
|
||||||
canvas.drawColor(Color.parseColor("#121212"))
|
canvas.drawColor(Color.parseColor("#121212"))
|
||||||
stars.forEach { canvas.drawCircle(it.x, it.y, it.radius, starPaint) }
|
stars.forEach { canvas.drawCircle(it.x, it.y, it.radius, starPaint) }
|
||||||
|
|
||||||
enemyBullets.forEach { canvas.drawLine(it.x, it.y, it.x, it.y + 20f, rocketPaint) }
|
val enemyBulletPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.RED
|
||||||
|
strokeWidth = 6f // same as player bullets
|
||||||
|
}
|
||||||
|
enemyBullets.forEach { canvas.drawLine(it.x, it.y, it.x, it.y + 20f, enemyBulletPaint) }
|
||||||
|
|
||||||
|
// Enemy rockets
|
||||||
rockets.forEach { rocket ->
|
rockets.forEach { rocket ->
|
||||||
rocket.trail.forEachIndexed { index, (x, y) ->
|
rocket.trail.forEachIndexed { index, (x, y) ->
|
||||||
val alpha = ((1f - index / 20f.toFloat()) * 255).toInt()
|
val alpha = ((1f - index / 20f.toFloat()) * 255).toInt()
|
||||||
|
@ -310,6 +613,26 @@ class SecretView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
rocketPaint.alpha = 255
|
rocketPaint.alpha = 255
|
||||||
|
|
||||||
|
// Player rockets
|
||||||
|
val missilePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = colorSecondary
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 3f
|
||||||
|
}
|
||||||
|
|
||||||
|
playerMissiles.forEach { missile ->
|
||||||
|
missile.trail.forEachIndexed { index, (x, y) ->
|
||||||
|
val alpha = ((1f - index / 20f.toFloat()) * 255).toInt()
|
||||||
|
missilePaint.alpha = alpha
|
||||||
|
canvas.drawCircle(x, y, 2f, missilePaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missilePaint.alpha = 255
|
||||||
|
playerMissiles.forEach { missile ->
|
||||||
|
canvas.drawCircle(missile.x, missile.y, 10f, missilePaint)
|
||||||
|
}
|
||||||
|
|
||||||
explosions.forEach {
|
explosions.forEach {
|
||||||
val radius = 40f * (1f - it.timer / 12f.toFloat())
|
val radius = 40f * (1f - it.timer / 12f.toFloat())
|
||||||
val alpha = (255 * (it.timer / 12f.toFloat())).toInt()
|
val alpha = (255 * (it.timer / 12f.toFloat())).toInt()
|
||||||
|
@ -326,7 +649,72 @@ class SecretView @JvmOverloads constructor(
|
||||||
enemies.forEach { it.draw(canvas, enemyPaint) }
|
enemies.forEach { it.draw(canvas, enemyPaint) }
|
||||||
rockets.forEach { canvas.drawCircle(it.x, it.y, 10f, rocketPaint) }
|
rockets.forEach { canvas.drawCircle(it.x, it.y, 10f, rocketPaint) }
|
||||||
|
|
||||||
|
// Draw pickups
|
||||||
|
pickups.forEach { pickup ->
|
||||||
|
// Draw cube (filled rect with outline)
|
||||||
|
val size = 30f
|
||||||
|
val left = pickup.x - size / 2f
|
||||||
|
val top = pickup.y - size / 2f
|
||||||
|
val right = pickup.x + size / 2f
|
||||||
|
val bottom = pickup.y + size / 2f
|
||||||
|
|
||||||
|
val hsv = floatArrayOf(pickup.hue, 1f, 1f)
|
||||||
|
val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.HSVToColor(hsv)
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.WHITE
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawRect(left, top, right, bottom, fillPaint)
|
||||||
|
canvas.drawRect(left, top, right, bottom, outlinePaint)
|
||||||
|
|
||||||
|
// Draw icon
|
||||||
|
val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.WHITE
|
||||||
|
strokeWidth = 3f
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(pickup.x, pickup.y)
|
||||||
|
canvas.rotate(-45f)
|
||||||
|
|
||||||
|
when (pickup.type) {
|
||||||
|
0 -> { // Multi-fire: two parallel lines
|
||||||
|
canvas.drawLine(-8f, -10f, -8f, 10f, iconPaint)
|
||||||
|
canvas.drawLine(8f, -10f, 8f, 10f, iconPaint)
|
||||||
|
}
|
||||||
|
1 -> { // Shield: quarter circle
|
||||||
|
val path = Path()
|
||||||
|
path.addArc(RectF(-10f, -10f, 10f, 10f), -90f, 90f)
|
||||||
|
canvas.drawPath(path, iconPaint)
|
||||||
|
}
|
||||||
|
2 -> { // Missiles: circle + trail
|
||||||
|
canvas.drawCircle(0f, 0f, 4f, iconPaint)
|
||||||
|
canvas.drawLine(-6f, 6f, -2f, 2f, iconPaint)
|
||||||
|
}
|
||||||
|
3 -> { // Rapid fire: two lines in succession
|
||||||
|
canvas.drawLine(-5f, -10f, -5f, 10f, iconPaint)
|
||||||
|
canvas.drawLine(5f, -10f, 5f, 10f, iconPaint)
|
||||||
|
}
|
||||||
|
4 -> { // Piercing bullets: arrow head
|
||||||
|
val path = Path()
|
||||||
|
path.moveTo(-8f, 10f)
|
||||||
|
path.lineTo(0f, -10f)
|
||||||
|
path.lineTo(8f, 10f)
|
||||||
|
canvas.drawPath(path, iconPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
if (!gameOver) {
|
if (!gameOver) {
|
||||||
|
// Draw player
|
||||||
val baseY = viewHeight - 100f
|
val baseY = viewHeight - 100f
|
||||||
val safeX = max(30f, min(playerX, viewWidth - 30f))
|
val safeX = max(30f, min(playerX, viewWidth - 30f))
|
||||||
playerX = safeX
|
playerX = safeX
|
||||||
|
@ -337,6 +725,18 @@ class SecretView @JvmOverloads constructor(
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
canvas.drawPath(path, playerPaint)
|
canvas.drawPath(path, playerPaint)
|
||||||
|
|
||||||
|
// Draw shield
|
||||||
|
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||||
|
val baseColor = colorSecondary
|
||||||
|
val shieldPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = baseColor
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 6f
|
||||||
|
alpha = (180 + shieldFlashAlpha * 75f).toInt().coerceAtMost(255)
|
||||||
|
}
|
||||||
|
canvas.drawCircle(playerX, viewHeight - 100f, 60f, shieldPaint)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
canvas.drawText("Game Over", viewWidth / 2f, viewHeight / 2f - 60f, textPaint)
|
canvas.drawText("Game Over", viewWidth / 2f, viewHeight / 2f - 60f, textPaint)
|
||||||
canvas.drawText("Score: $score", viewWidth / 2f, viewHeight / 2f + 10f, textPaint)
|
canvas.drawText("Score: $score", viewWidth / 2f, viewHeight / 2f + 10f, textPaint)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue