From b65222ddf94ab26650edfa579284d219692ce6d8 Mon Sep 17 00:00:00 2001 From: partisan Date: Sun, 25 May 2025 21:59:07 +0200 Subject: [PATCH] Important stuff --- .../partisan/weforge/xyz/pulse/SecretView.kt | 468 ++++++++++++++++-- 1 file changed, 434 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/SecretView.kt b/app/src/main/java/partisan/weforge/xyz/pulse/SecretView.kt index 51550e8..64efcce 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/SecretView.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/SecretView.kt @@ -52,16 +52,33 @@ class SecretView @JvmOverloads constructor( textAlign = Paint.Align.CENTER textSize = 48f } + private var colorSecondary: Int = Color.GREEN private var playerX = 0f private var viewWidth = 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 shieldRechargeTimer = 0L + private var shieldFlashAlpha = 0f + private var lastMissileSide = -1 + private var missileCooldown = 0L private var bulletCooldownMs = 0L private var gameOver = false 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() private val enemyBullets = mutableListOf() private val enemies = mutableListOf() @@ -69,11 +86,29 @@ class SecretView @JvmOverloads constructor( private val stars = mutableListOf() private val explosions = mutableListOf() private val rocketTrails = mutableListOf>() + private val pickups = mutableListOf() + private val playerMissiles = mutableListOf() - 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 Rocket(var x: Float, var y: Float, var angle: Float, val trail: MutableList> = mutableListOf()) 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> = mutableListOf() + ) private var lastLogicTime = 0L 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)) } - 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 { - val primary = it.getColor(0, Color.CYAN) - playerPaint.color = primary - enemyPaint.color = primary + playerPaint.color = it.getColor(0, Color.CYAN) + enemyPaint.color = it.getColor(0, Color.CYAN) + colorSecondary = it.getColor(1, Color.GREEN) } 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 -> rocket.trail.add(0, rocket.x to rocket.y) if (rocket.trail.size > 20) { @@ -139,14 +191,27 @@ class SecretView @JvmOverloads constructor( explosions.forEach { it.timer-- } explosions.removeIf { it.timer <= 0 } - bullets.forEach { it.y += it.dy * deltaMs / 16f } - bullets.removeIf { it.y < 0 } + bullets.removeIf { it.y < 0 || it.life <= 0 } bulletCooldownMs -= deltaMs if (isTouching && bulletCooldownMs <= 0) { - bullets.add(Bullet(playerX, viewHeight - 130f)) - bulletCooldownMs = 150L + val baseCooldown = 450f + 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 { @@ -155,6 +220,12 @@ class SecretView @JvmOverloads constructor( 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 { val targetAngle = atan2(viewHeight - 100f - it.y, playerX - it.x) it.angle += ((targetAngle - it.angle + PI).mod(2 * PI) - PI).toFloat() * 0.1f @@ -165,59 +236,238 @@ class SecretView @JvmOverloads constructor( enemyBullets.forEach { it.y += it.dy * deltaMs / 16f } enemyBullets.removeIf { it.y > viewHeight } - + + updatePlayerMissiles(deltaMs) + checkCollisions() spawnEnemies(deltaMs) } 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() while (enemyIter.hasNext()) { val enemy = enemyIter.next() if (hypot(playerX - enemy.x, viewHeight - 100f - enemy.y) < 40f) { - explosions.add(Explosion(playerX, viewHeight - 100f)) - gameOver = true - return + if (shieldLevel > 0 && shieldRechargeTimer <= 0) { + shieldRechargeTimer = calculateShieldRechargeTime() + enemyIter.remove() + explosions.add(Explosion(enemy.x, enemy.y)) + continue + } else { + explosions.add(Explosion(playerX, viewHeight - 100f)) + gameOver = true + return + } } for (b in bullets) { if (hypot(b.x - enemy.x, b.y - enemy.y) < 30f) { explosions.add(Explosion(enemy.x, enemy.y)) enemyIter.remove() 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 } } } - for (b in enemyBullets) { - if (hypot(b.x - playerX, b.y - (viewHeight - 100f)) < 20f) { - explosions.add(Explosion(playerX, viewHeight - 100f)) - gameOver = true - return + val bulletIter = enemyBullets.iterator() + while (bulletIter.hasNext()) { + val b = bulletIter.next() + val dy = b.y - (viewHeight - 100f) + 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() while (rocketIter.hasNext()) { val rocket = rocketIter.next() - if (hypot(playerX - rocket.x, viewHeight - 100f - rocket.y) < 30f) { - explosions.add(Explosion(playerX, viewHeight - 100f)) - gameOver = true - return + val dy = viewHeight - 100f - rocket.y + val dx = playerX - rocket.x + val dist = hypot(dx, dy) + 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) { if (hypot(b.x - rocket.x, b.y - rocket.y) < 20f) { explosions.add(Explosion(rocket.x, rocket.y)) rocketIter.remove() - score += 15 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) { waveTimer -= deltaMs + adaptiveSpawnTimer += deltaMs // Passive asteroids if (Random.nextFloat() < 0.002f) { @@ -233,40 +483,67 @@ class SecretView @JvmOverloads constructor( else -> "hard" } - enemiesLeftInWave = when (currentWaveType) { - "easy" -> 3 + currentWave - "medium" -> 2 + currentWave / 2 - "hard" -> 1 + currentWave / 4 - else -> 3 + val difficultyBoost = when (currentWaveType) { + "easy" -> (currentWave * 1.2f).roundToInt() + "medium" -> (currentWave * 1.5f).roundToInt() + "hard" -> (currentWave * 1.8f).roundToInt() + else -> currentWave } + enemiesLeftInWave = 3 + difficultyBoost 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 - 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 baseY = -40f val spacing = 35f val sharedOffset = Random.nextFloat() * 1000f val sharedFireTime = Random.nextLong(2000L, 4000L) - val formationSize = when (currentWaveType) { + // Dynamically adjust formation size + val formationBase = when (currentWaveType) { "easy" -> 5 "medium" -> 3 "hard" -> 1 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 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) { "easy" -> EnemyEasy(x, baseY) "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) } @@ -283,10 +560,31 @@ class SecretView @JvmOverloads constructor( rockets.clear() explosions.clear() rocketTrails.clear() + playerMissiles.clear() + pickups.clear() + + // Reset upgrades + multiFireLevel = 1 + piercingLevel = 1 + shieldLevel = 0 + missileLevel = 0 + rapidFireLevel = 1 + + // Reset gameplay state score = 0 currentWave = 0 enemiesLeftInWave = 0 gameOver = false + shieldRechargeTimer = 0L + shieldFlashAlpha = 0f + lastMissileSide = -1 + missileCooldown = 0L + bulletCooldownMs = 0L + enemyClearAggression = 0f + adaptiveSpawnTimer = 0L + lastEnemyCount = 0 + avgEnemiesPerSecond = 0f + playerX = viewWidth / 2f lastLogicTime = System.nanoTime() Choreographer.getInstance().postFrameCallback(this) @@ -299,8 +597,13 @@ class SecretView @JvmOverloads constructor( canvas.drawColor(Color.parseColor("#121212")) 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 -> rocket.trail.forEachIndexed { index, (x, y) -> val alpha = ((1f - index / 20f.toFloat()) * 255).toInt() @@ -310,6 +613,26 @@ class SecretView @JvmOverloads constructor( } 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 { val radius = 40f * (1f - it.timer / 12f.toFloat()) val alpha = (255 * (it.timer / 12f.toFloat())).toInt() @@ -326,7 +649,72 @@ class SecretView @JvmOverloads constructor( enemies.forEach { it.draw(canvas, enemyPaint) } 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) { + // Draw player val baseY = viewHeight - 100f val safeX = max(30f, min(playerX, viewWidth - 30f)) playerX = safeX @@ -337,6 +725,18 @@ class SecretView @JvmOverloads constructor( close() } 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 { canvas.drawText("Game Over", viewWidth / 2f, viewHeight / 2f - 60f, textPaint) canvas.drawText("Score: $score", viewWidth / 2f, viewHeight / 2f + 10f, textPaint)