Code Sketch


sniper_0718
By: Mhalsakant School
Category: Art
import net.kogics.kojo.lite.i18n.SwedishAPI
import java.awt.{Color, Font, Graphics2D, RenderingHints}
import java.awt.event.{KeyEvent, KeyListener}
import javax.swing.Timer
import java.awt.event.{ActionListener, ActionEvent}
import scala.math._

// Complete Self-Contained Kojo 3D Horror Game
object KojoHorrorGame {

  val ScreenWidth = 640
  val ScreenHeight = 480

  // 1. Map Layout (1 = Wall, 0 = Empty Path)
  val mapWidth = 12
  val mapHeight = 12
  val map: Array[Array[Int]] = Array(
    Array(1,1,1,1,1,1,1,1,1,1,1,1),
    Array(1,0,0,0,0,0,1,0,0,0,0,1),
    Array(1,0,1,1,1,0,1,0,1,1,0,1),
    Array(1,0,1,0,0,0,0,0,0,1,0,1),
    Array(1,0,1,0,1,1,1,1,0,1,0,1),
    Array(1,0,0,0,1,0,0,1,0,0,0,1),
    Array(1,1,1,0,1,0,0,1,1,1,0,1),
    Array(1,0,0,0,0,0,0,0,0,1,0,1),
    Array(1,0,1,1,1,1,1,1,0,1,0,1),
    Array(1,0,0,0,0,0,0,0,0,0,0,1),
    Array(1,0,1,1,1,1,1,1,1,1,0,1),
    Array(1,1,1,1,1,1,1,1,1,1,1,1)
  )

  // Game States
  var gameStarted = false
  var gameOver = false

  // Player Settings
  var playerX = 1.5
  var playerY = 1.5
  var playerA = 0.0
  val fov = Pi / 3.0

  // Killer Settings
  var enemyX = 10.5
  var enemyY = 10.5
  val enemySpeed = 0.02

  // Controls
  var wPressed, sPressed, aPressed, dPressed = false
  var leftKey, rightKey = false

  // Canvas Window Setup
  val frame = new javax.swing.JFrame("Kojo 3D First-Person Horror")
  val panel = new javax.swing.JPanel() {
    override def paintComponent(gNative: java.awt.Graphics): Unit = {
      super.paintComponent(gNative)
      val g = gNative.asInstanceOf[Graphics2D]
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)

      if (!gameStarted) {
        // --- START SCREEN ---
        g.setColor(Color.BLACK)
        g.fillRect(0, 0, ScreenWidth, ScreenHeight)
        
        g.setColor(Color.RED)
        g.setFont(new Font("Arial", Font.BOLD, 36))
        g.drawString("3D HORROR ESCAPE", ScreenWidth / 2 - 180, ScreenHeight / 2 - 40)

        g.setColor(Color.WHITE)
        g.setFont(new Font("Arial", Font.PLAIN, 20))
        g.drawString("Press ENTER to Start Game", ScreenWidth / 2 - 130, ScreenHeight / 2 + 20)
        g.setFont(new Font("Arial", Font.PLAIN, 14))
        g.drawString("Controls: W,A,S,D to Walk | Left/Right Arrows to Turn", ScreenWidth / 2 - 170, ScreenHeight / 2 + 60)

      } else if (gameOver) {
        // --- GAME OVER SCREEN ---
        g.setColor(new Color(150, 0, 0))
        g.fillRect(0, 0, ScreenWidth, ScreenHeight)

        g.setColor(Color.WHITE)
        g.setFont(new Font("Arial", Font.BOLD, 36))
        g.drawString("YOU WERE CAUGHT!", ScreenWidth / 2 - 180, ScreenHeight / 2 - 20)

        g.setFont(new Font("Arial", Font.PLAIN, 18))
        g.drawString("Press ENTER to Try Again", ScreenWidth / 2 - 120, ScreenHeight / 2 + 40)

      } else {
        // --- 3D RENDERING ENGINE ---
        // Ceiling & Floor
        g.setColor(new Color(15, 15, 25))
        g.fillRect(0, 0, ScreenWidth, ScreenHeight / 2)
        g.setColor(new Color(35, 35, 35))
        g.fillRect(0, ScreenHeight / 2, ScreenWidth, ScreenHeight / 2)

        val depthBuffer = new Array[Double](ScreenWidth)

        // Wall Raycasting
        for (x <- 0 until ScreenWidth) {
          val rayAngle = (playerA - fov / 2.0) + (x.toDouble / ScreenWidth.toDouble) * fov
          var distanceToWall = 0.0
          var hitWall = false

          val eyeX = cos(rayAngle)
          val eyeY = sin(rayAngle)

          while (!hitWall && distanceToWall < 16.0) {
            distanceToWall += 0.05
            val checkX = (playerX + eyeX * distanceToWall).toInt
            val checkY = (playerY + eyeY * distanceToWall).toInt

            if (checkX < 0 || checkX >= mapWidth || checkY < 0 || checkY >= mapHeight) {
              hitWall = true
              distanceToWall = 16.0
            } else if (map(checkY)(checkX) == 1) {
              hitWall = true
            }
          }

          val correctedDist = distanceToWall * cos(rayAngle - playerA)
          depthBuffer(x) = correctedDist

          val ceiling = (ScreenHeight / 2.0) - (ScreenHeight / correctedDist)
          val floor = ScreenHeight - ceiling
          val wallHeight = floor - ceiling

          val shade = max(0, min(255, (255 - correctedDist * 18).toInt))
          g.setColor(new Color(shade, shade / 3, 30))
          g.fillRect(x, ceiling.toInt, 1, wallHeight.toInt)
        }

        // Render 3D Killer (Enemy)
        val ex = enemyX - playerX
        val ey = enemyY - playerY

        var enemyAngle = atan2(ey, ex) - playerA
        while (enemyAngle < -Pi) enemyAngle += 2 * Pi
        while (enemyAngle > Pi) enemyAngle -= 2 * Pi

        val enemyDist = sqrt(ex * ex + ey * ey)

        if (enemyDist > 0.2 && abs(enemyAngle) < fov) {
          val screenX = ((ScreenWidth / 2) * (1 + tan(enemyAngle) / tan(fov / 2))).toInt
          val spriteSize = min(ScreenWidth, (ScreenHeight / enemyDist).toInt)
          val spriteTop = (ScreenHeight / 2) - spriteSize / 2

          val startX = max(0, screenX - spriteSize / 2)
          val endX = min(ScreenWidth - 1, screenX + spriteSize / 2)

          for (x <- startX to endX) {
            if (x >= 0 && x < ScreenWidth && depthBuffer(x) > enemyDist) {
              // Red Killer Body
              g.setColor(new Color(230, 20, 20))
              g.fillRect(x, spriteTop, 1, spriteSize)

              // Glowing Yellow Eyes
              g.setColor(Color.YELLOW)
              val eyeSize = max(2, spriteSize / 8)
              g.fillRect(x, spriteTop + spriteSize / 4, 1, eyeSize)
            }
          }
        }

        // Mini Map (Top-Left Corner)
        g.setColor(new Color(0, 0, 0, 180))
        g.fillRect(10, 10, 60, 60)
        g.setColor(Color.RED)
        g.fillRect(10 + (enemyX * 5).toInt, 10 + (enemyY * 5).toInt, 4, 4)
        g.setColor(Color.GREEN)
        g.fillRect(10 + (playerX * 5).toInt, 10 + (playerY * 5).toInt, 4, 4)
      }
    }
  }

  // Update Logic (60 FPS)
  val gameLoop = new Timer(16, new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      if (gameStarted && !gameOver) {
        val moveSpeed = 0.05
        val rotSpeed = 0.04

        if (leftKey) playerA -= rotSpeed
        if (rightKey) playerA += rotSpeed

        var newX = playerX
        var newY = playerY

        if (wPressed) { newX += cos(playerA) * moveSpeed; newY += sin(playerA) * moveSpeed }
        if (sPressed) { newX -= cos(playerA) * moveSpeed; newY -= sin(playerA) * moveSpeed }
        if (aPressed) { newX += sin(playerA) * moveSpeed; newY -= cos(playerA) * moveSpeed }
        if (dPressed) { newX -= sin(playerA) * moveSpeed; newY += cos(playerA) * moveSpeed }

        if (map(newY.toInt)(playerX.toInt) == 0) playerY = newY
        if (map(playerY.toInt)(newX.toInt) == 0) playerX = newX

        // Killer AI movement
        val dx = playerX - enemyX
        val dy = playerY - enemyY
        val dist = sqrt(dx * dx + dy * dy)

        if (dist > 0.4) {
          val enemyNewX = enemyX + (dx / dist) * enemySpeed
          val enemyNewY = enemyY + (dy / dist) * enemySpeed

          if (map(enemyY.toInt)(enemyNewX.toInt) == 0) enemyX = enemyNewX
          if (map(enemyNewY.toInt)(enemyX.toInt) == 0) enemyY = enemyNewY
        } else {
          gameOver = true
        }
      }
      panel.repaint()
    }
  })

  def resetGame(): Unit = {
    playerX = 1.5
    playerY = 1.5
    playerA = 0.0
    enemyX = 10.5
    enemyY = 10.5
    gameOver = false
    gameStarted = true
  }

  def run(): Unit = {
    panel.setPreferredSize(new java.awt.Dimension(ScreenWidth, ScreenHeight))
    panel.setFocusable(true)

    panel.addKeyListener(new KeyListener {
      override def keyTyped(e: KeyEvent): Unit = {}

      override def keyPressed(e: KeyEvent): Unit = {
        e.getKeyCode match {
          case KeyEvent.VK_ENTER =>
            if (!gameStarted || gameOver) resetGame()
          case KeyEvent.VK_W     => wPressed = true
          case KeyEvent.VK_S     => sPressed = true
          case KeyEvent.VK_A     => aPressed = true
          case KeyEvent.VK_D     => dPressed = true
          case KeyEvent.VK_LEFT  => leftKey = true
          case KeyEvent.VK_RIGHT => rightKey = true
          case _ =>
        }
      }

      override def keyReleased(e: KeyEvent): Unit = {
        e.getKeyCode match {
          case KeyEvent.VK_W     => wPressed = false
          case KeyEvent.VK_S     => sPressed = false
          case KeyEvent.VK_A     => aPressed = false
          case KeyEvent.VK_D     => dPressed = false
          case KeyEvent.VK_LEFT  => leftKey = false
          case KeyEvent.VK_RIGHT => rightKey = false
          case _ =>
        }
      }
    })

    frame.setContentPane(panel)
    frame.pack()
    frame.setLocationRelativeTo(null)
    frame.setVisible(true)
    panel.requestFocusInWindow()
    gameLoop.start()
  }
}

// Start Game
KojoHorrorGame.run()