CCCPaste
// Package main implements Brkout - a classic Breakout game for Dreamcast.
// This is a port of the arcade classic using Go and KOS.
// Ported from https://3e8.org/hacks/brkout/
package main

import "kos"

// Screen dimensions
const (
	ScreenWidth  = 640
	ScreenHeight = 480
)

// Playing field configuration
const (
	FieldX      = 10
	FieldY      = 40
	FieldWidth  = 500
	FieldHeight = 420
)

// Brick grid configuration
const (
	BrickColumns = 20
	BrickRows    = 15
	BrickWidth   = 25
	BrickHeight  = 12
)

// Paddle configuration
const (
	PaddleWidth  = 70
	PaddleHeight = 10
	PaddleSpeed  = 6.0
	PaddleY      = FieldHeight - 30
)

// Ball configuration
const (
	BallRadius       = 6
	BallInitialSpeed = 3.0
	BallMaxSpeed     = 7.0
	BallSpeedStep    = 0.5
)

// Input configuration
const (
	JoystickDeadzone    = 15
	JoystickSensitivity = 18.0
)

// Game rules
const (
	InitialLives   = 3
	PointsPerBrick = 10
)

// Audio settings
const (
	SoundVolume = 160
	SoundCenter = 128
)

// Textures loaded from romdisk
var (
	titleTexture *kos.PlxTexture
	fontTexture  *kos.PlxTexture
	fieldTexture *kos.PlxTexture
)

// Sound effects
var (
	clickSound  kos.SfxHandle
	bounceSound kos.SfxHandle
	hitSound    kos.SfxHandle
	loseSound   kos.SfxHandle
	winSound    kos.SfxHandle
)

// Controller state
var (
	currentButtons  uint32
	previousButtons uint32
	joystickX       int32
)

// Game state
var (
	paddleX    float32
	ballX      float32
	ballY      float32
	ballSpeedX float32
	ballSpeedY float32
	bricks     [BrickColumns * BrickRows]int
	bricksLeft int
	lives      int
	score      int
	level      int
)

// Brick color palette
var brickColors []uint32

// ----------------------------------------------------------------------------
// Math helpers
// ----------------------------------------------------------------------------

func abs(x float32) float32 {
	if x < 0 {
		return -x
	}
	return x
}

// ----------------------------------------------------------------------------
// Input handling
// ----------------------------------------------------------------------------

func readController() {
	previousButtons = currentButtons

	controller := kos.MapleEnumType(0, kos.MAPLE_FUNC_CONTROLLER)
	if controller == nil {
		currentButtons, joystickX = 0, 0
		return
	}

	state := controller.ContState()
	if state == nil {
		currentButtons, joystickX = 0, 0
		return
	}

	currentButtons = state.Buttons
	joystickX = state.Joyx
}

func buttonPressed(button uint32) bool {
	wasUp := (previousButtons & button) == 0
	isDown := (currentButtons & button) != 0
	return wasUp && isDown
}

func buttonHeld(button uint32) bool {
	return (currentButtons & button) != 0
}

func getJoystickMovement() float32 {
	if joystickX > -JoystickDeadzone && joystickX < JoystickDeadzone {
		return 0
	}
	return float32(joystickX) / JoystickSensitivity
}

// ----------------------------------------------------------------------------
// Drawing primitives (using Plx library like other examples)
// ----------------------------------------------------------------------------

func drawColoredQuad(x, y, width, height, z float32, color uint32) {
	kos.PlxVertInp(kos.PLX_VERT, x, y+height, z, color)
	kos.PlxVertInp(kos.PLX_VERT, x, y, z, color)
	kos.PlxVertInp(kos.PLX_VERT, x+width, y+height, z, color)
	kos.PlxVertInp(kos.PLX_VERT_EOS, x+width, y, z, color)
}

func drawTexturedQuad(x, y, width, height, z float32, color uint32, u1, v1, u2, v2 float32) {
	kos.PlxVertIfp(kos.PLX_VERT, x, y+height, z, color, u1, v2)
	kos.PlxVertIfp(kos.PLX_VERT, x, y, z, color, u1, v1)
	kos.PlxVertIfp(kos.PLX_VERT, x+width, y+height, z, color, u2, v2)
	kos.PlxVertIfp(kos.PLX_VERT_EOS, x+width, y, z, color, u2, v1)
}

func setupColoredContext(list uint32) {
	kos.PlxCxtInit()
	kos.PlxCxtTexture(nil)
	kos.PlxCxtCulling(kos.PLX_CULL_NONE)
	kos.PlxCxtSend(int32(list))
}

func setupTexturedContext(list uint32, texture *kos.PlxTexture) {
	kos.PlxCxtInit()
	kos.PlxCxtTexture(texture.Ptr())
	kos.PlxCxtCulling(kos.PLX_CULL_NONE)
	kos.PlxCxtSend(int32(list))
}

// ----------------------------------------------------------------------------
// Text rendering (bitmap font from texture)
// ----------------------------------------------------------------------------

const (
	CharWidth  = 12
	CharHeight = 24
	FontSize   = 256.0
)

func drawString(x, y, z float32, color uint32, text string) {
	for i, char := range text {
		if char < 32 {
			continue
		}
		column := int(char) % 16
		row := int(char) / 16
		u1 := float32(column*CharWidth) / FontSize
		v1 := float32(row*CharHeight) / FontSize
		u2 := u1 + CharWidth/FontSize
		v2 := v1 + CharHeight/FontSize
		drawTexturedQuad(x+float32(i*CharWidth), y, CharWidth, CharHeight, z, color, u1, v1, u2, v2)
	}
}

func drawInteger(x, y, z float32, color uint32, value int) {
	if value == 0 {
		drawString(x, y, z, color, "0")
		return
	}

	text := ""
	for value > 0 {
		digit := byte(value % 10)
		text = string('0'+digit) + text
		value = value / 10
	}
	drawString(x, y, z, color, text)
}

// ----------------------------------------------------------------------------
// Asset loading
// ----------------------------------------------------------------------------

func loadAssets() {
	kos.PvrInitDefaults()

	// Load textures from romdisk
	titleTexture = kos.PlxTxrLoad("/rd/brkout.png", false, 0)
	fontTexture = kos.PlxTxrLoad("/rd/font.png", true, 0)
	fieldTexture = kos.PlxTxrLoad("/rd/field.png", false, 0)

	// Load sound effects
	kos.SndStreamInit()
	clickSound = kos.SndSfxLoad("/rd/click.wav")
	bounceSound = kos.SndSfxLoad("/rd/bounce.wav")
	hitSound = kos.SndSfxLoad("/rd/toggled.wav")
	loseSound = kos.SndSfxLoad("/rd/failure.wav")
	winSound = kos.SndSfxLoad("/rd/success.wav")

	// Initialize brick color palette (rainbow gradient)
	brickColors = []uint32{
		kos.PlxPackColor(1, 1.0, 0.2, 0.2), // Red
		kos.PlxPackColor(1, 1.0, 0.6, 0.2), // Orange
		kos.PlxPackColor(1, 1.0, 1.0, 0.2), // Yellow
		kos.PlxPackColor(1, 0.2, 1.0, 0.2), // Green
		kos.PlxPackColor(1, 0.2, 1.0, 1.0), // Cyan
		kos.PlxPackColor(1, 0.2, 0.2, 1.0), // Blue
		kos.PlxPackColor(1, 0.8, 0.2, 1.0), // Purple
	}
}

// ----------------------------------------------------------------------------
// Colors (pre-computed for performance)
// ----------------------------------------------------------------------------

func colorWhite() uint32      { return kos.PlxPackColor(1, 1, 1, 1) }
func colorGray() uint32       { return kos.PlxPackColor(1, 0.5, 0.5, 0.5) }
func colorBackground() uint32 { return kos.PlxPackColor(1, 0.05, 0.05, 0.1) }
func colorWall() uint32       { return kos.PlxPackColor(1, 0.4, 0.4, 0.6) }
func colorPaddle() uint32     { return kos.PlxPackColor(1, 0.4, 0.4, 0.7) }
func colorShadow() uint32     { return kos.PlxPackColor(0.7, 0, 0, 0) }
func colorGameOverBg() uint32 { return kos.PlxPackColor(1, 0.1, 0.1, 0.15) }

// ----------------------------------------------------------------------------
// Title Menu
// ----------------------------------------------------------------------------

func showTitleMenu() bool {
	kos.PvrSetBgColor(0, 0, 0)
	selection := 0

	for {
		readController()

		// Navigate menu
		if buttonPressed(kos.CONT_DPAD_UP) || buttonPressed(kos.CONT_DPAD_DOWN) {
			selection = 1 - selection
			kos.SndSfxPlay(clickSound, SoundVolume, SoundCenter)
		}

		// Confirm selection
		if buttonPressed(kos.CONT_A) || buttonPressed(kos.CONT_START) {
			kos.SndSfxPlay(hitSound, SoundVolume, SoundCenter)
			fadeOutMenu(selection)
			return selection == 1 // true = quit
		}

		renderTitleMenu(selection, 1.0)
	}
}

func fadeOutMenu(selection int) {
	for frame := 30; frame >= 0; frame-- {
		alpha := float32(frame) / 30.0
		renderTitleMenu(selection, alpha)
	}
}

func renderTitleMenu(selection int, alpha float32) {
	kos.PvrWaitReady()
	kos.PvrSceneBegin()
	kos.PvrListBegin(kos.PVR_LIST_OP_POLY)

	// Title screen background
	if titleTexture != nil {
		setupTexturedContext(kos.PVR_LIST_OP_POLY, titleTexture)
		tintColor := kos.PlxPackColor(1, alpha, alpha, alpha)
		drawTexturedQuad(0, 0, ScreenWidth, ScreenHeight, 100, tintColor,
			0, 0, 640.0/1024.0, 480.0/512.0)
	}
	kos.PvrListFinish()

	// Menu cursor
	kos.PvrListBegin(kos.PVR_LIST_TR_POLY)
	setupColoredContext(kos.PVR_LIST_TR_POLY)
	cursorColor := kos.PlxPackColor(alpha, 1, 1, 1)
	cursorY := float32(314 + selection*50)
	drawColoredQuad(250, cursorY, 20, 20, 200, cursorColor)
	kos.PvrListFinish()

	kos.PvrSceneFinish()
}

// ----------------------------------------------------------------------------
// Game Logic
// ----------------------------------------------------------------------------

func startNewGame() {
	lives = InitialLives
	score = 0
	level = 1
	setupLevel()
}

func setupLevel() {
	// Center paddle
	paddleX = float32(FieldWidth-PaddleWidth) / 2.0
	resetBall()

	// Create brick grid (more rows at higher levels)
	rows := 3 + level
	if rows > BrickRows {
		rows = BrickRows
	}

	bricksLeft = 0
	colorCount := len(brickColors)

	for row := 0; row < BrickRows; row++ {
		for col := 0; col < BrickColumns; col++ {
			index := row*BrickColumns + col
			if row < rows {
				bricks[index] = (row % colorCount) + 1
				bricksLeft++
			} else {
				bricks[index] = 0
			}
		}
	}
}

func resetBall() {
	ballX = paddleX + float32(PaddleWidth)/2.0 - float32(BallRadius)
	ballY = float32(FieldHeight) - 50.0

	speed := BallInitialSpeed + float32(level)*BallSpeedStep
	if speed > BallMaxSpeed {
		speed = BallMaxSpeed
	}
	ballSpeedX = speed * 0.6
	ballSpeedY = -speed
}

func runGameLoop() {
	kos.PvrSetBgColor(0.05, 0.05, 0.1)
	startNewGame()

	for lives > 0 {
		readController()

		// Exit to menu on START+B
		if buttonHeld(kos.CONT_START) && buttonHeld(kos.CONT_B) {
			return
		}

		updatePaddle()
		updateBall()
		renderGameFrame()
	}
}

func updatePaddle() {
	// D-pad movement
	if buttonHeld(kos.CONT_DPAD_LEFT) {
		paddleX -= PaddleSpeed
	}
	if buttonHeld(kos.CONT_DPAD_RIGHT) {
		paddleX += PaddleSpeed
	}

	// Analog stick movement
	paddleX += getJoystickMovement()

	// Clamp to field bounds
	if paddleX < 0 {
		paddleX = 0
	}
	maxX := float32(FieldWidth - PaddleWidth)
	if paddleX > maxX {
		paddleX = maxX
	}
}

func updateBall() {
	ballX += ballSpeedX
	ballY += ballSpeedY

	handleWallCollisions()
	handlePaddleCollision()
	handleBrickCollisions()
	handleBallLost()
	handleLevelComplete()
}

func handleWallCollisions() {
	// Left wall
	if ballX < 0 {
		ballX = 0
		ballSpeedX = -ballSpeedX
		kos.SndSfxPlay(bounceSound, 140, 64)
	}

	// Right wall
	maxX := float32(FieldWidth - BallRadius*2)
	if ballX > maxX {
		ballX = maxX
		ballSpeedX = -ballSpeedX
		kos.SndSfxPlay(bounceSound, 140, 192)
	}

	// Top wall
	if ballY < 0 {
		ballY = 0
		ballSpeedY = -ballSpeedY
		kos.SndSfxPlay(bounceSound, 140, SoundCenter)
	}
}

func handlePaddleCollision() {
	paddleTop := float32(PaddleY)
	ballBottom := ballY + float32(BallRadius*2)
	ballCenterX := ballX + float32(BallRadius)

	// Check if ball is at paddle height
	if ballBottom < paddleTop || ballY >= paddleTop+float32(PaddleHeight) {
		return
	}

	// Check if ball is horizontally aligned with paddle
	if ballCenterX < paddleX || ballCenterX >= paddleX+float32(PaddleWidth) {
		return
	}

	// Bounce off paddle
	ballY = paddleTop - float32(BallRadius*2)
	ballSpeedY = -abs(ballSpeedY)

	// Adjust horizontal speed based on hit position
	hitPosition := (ballCenterX - paddleX) / float32(PaddleWidth)
	ballSpeedX = (hitPosition - 0.5) * 8.0

	kos.SndSfxPlay(bounceSound, SoundVolume, SoundCenter)
}

func handleBrickCollisions() {
	ballCenterX := ballX + float32(BallRadius)
	ballCenterY := ballY + float32(BallRadius)

	// Convert ball position to brick grid coordinates
	brickCol := int(ballCenterX) / BrickWidth
	brickRow := int(ballCenterY) / BrickHeight

	// Check bounds
	if brickCol < 0 || brickCol >= BrickColumns || brickRow < 0 || brickRow >= BrickRows {
		return
	}

	index := brickRow*BrickColumns + brickCol
	if bricks[index] == 0 {
		return
	}

	// Destroy brick
	bricks[index] = 0
	bricksLeft--
	score += PointsPerBrick * level

	// Determine bounce direction
	brickCenterX := float32(brickCol*BrickWidth) + float32(BrickWidth)/2.0
	brickCenterY := float32(brickRow*BrickHeight) + float32(BrickHeight)/2.0

	horizontalOverlap := abs(ballCenterX-brickCenterX) / float32(BrickWidth)
	verticalOverlap := abs(ballCenterY-brickCenterY) / float32(BrickHeight)

	if horizontalOverlap > verticalOverlap {
		ballSpeedX = -ballSpeedX
	} else {
		ballSpeedY = -ballSpeedY
	}

	kos.SndSfxPlay(hitSound, 150, SoundCenter)
}

func handleBallLost() {
	if ballY <= float32(FieldHeight) {
		return
	}

	lives--
	kos.SndSfxPlay(loseSound, SoundVolume, SoundCenter)

	if lives <= 0 {
		showGameOver()
		return
	}

	resetBall()
	pauseFrames(60)
}

func handleLevelComplete() {
	if bricksLeft > 0 {
		return
	}

	kos.SndSfxPlay(winSound, SoundVolume, SoundCenter)
	level++
	setupLevel()
	pauseFrames(60)
}

func pauseFrames(count int) {
	for i := 0; i < count; i++ {
		renderGameFrame()
	}
}

// ----------------------------------------------------------------------------
// Game Over Screen
// ----------------------------------------------------------------------------

func showGameOver() {
	for frame := 0; frame < 180; frame++ {
		kos.PvrWaitReady()
		kos.PvrSceneBegin()

		// Background
		kos.PvrListBegin(kos.PVR_LIST_OP_POLY)
		setupColoredContext(kos.PVR_LIST_OP_POLY)
		drawColoredQuad(0, 0, ScreenWidth, ScreenHeight, 1, colorGameOverBg())
		kos.PvrListFinish()

		// Text
		kos.PvrListBegin(kos.PVR_LIST_TR_POLY)
		if fontTexture != nil {
			setupTexturedContext(kos.PVR_LIST_TR_POLY, fontTexture)
			drawString(220, 180, 100, colorWhite(), "GAME OVER")
			drawString(220, 230, 100, colorGray(), "Score:")
			drawInteger(320, 230, 100, colorWhite(), score)
		}
		kos.PvrListFinish()

		kos.PvrSceneFinish()

		readController()
		if buttonPressed(kos.CONT_A) {
			return
		}
	}
}

// ----------------------------------------------------------------------------
// Game Rendering
// ----------------------------------------------------------------------------

func renderGameFrame() {
	kos.PvrWaitReady()
	kos.PvrSceneBegin()

	// Opaque geometry
	kos.PvrListBegin(kos.PVR_LIST_OP_POLY)
	renderBackground()
	renderField()
	renderWalls()
	renderBricks()
	renderPaddle()
	renderBall()
	kos.PvrListFinish()

	// Translucent geometry (HUD text)
	kos.PvrListBegin(kos.PVR_LIST_TR_POLY)
	renderHUD()
	kos.PvrListFinish()

	kos.PvrSceneFinish()
}

func renderBackground() {
	setupColoredContext(kos.PVR_LIST_OP_POLY)
	drawColoredQuad(0, 0, ScreenWidth, ScreenHeight, 1, colorBackground())
}

func renderField() {
	if fieldTexture == nil {
		return
	}
	setupTexturedContext(kos.PVR_LIST_OP_POLY, fieldTexture)
	drawTexturedQuad(FieldX, FieldY, FieldWidth, FieldHeight, 50, colorWhite(),
		0, 0, float32(FieldWidth)/512.0, float32(FieldHeight)/512.0)
}

func renderWalls() {
	setupColoredContext(kos.PVR_LIST_OP_POLY)
	wall := colorWall()
	drawColoredQuad(FieldX-5, FieldY, 5, FieldHeight, 100, wall)          // Left
	drawColoredQuad(FieldX+FieldWidth, FieldY, 5, FieldHeight, 100, wall) // Right
	drawColoredQuad(FieldX-5, FieldY-5, FieldWidth+10, 5, 100, wall)      // Top
}

func renderBricks() {
	shadow := colorShadow()
	colorCount := len(brickColors)

	for row := 0; row < BrickRows; row++ {
		for col := 0; col < BrickColumns; col++ {
			colorIndex := bricks[row*BrickColumns+col]
			if colorIndex == 0 {
				continue
			}

			x := float32(FieldX + col*BrickWidth)
			y := float32(FieldY + row*BrickHeight)

			// Shadow
			drawColoredQuad(x+2, y+2, BrickWidth-2, BrickHeight-2, 99, shadow)
			// Brick
			drawColoredQuad(x, y, BrickWidth-2, BrickHeight-2, 100,
				brickColors[(colorIndex-1)%colorCount])
		}
	}
}

func renderPaddle() {
	x := float32(FieldX) + paddleX
	y := float32(FieldY + PaddleY)

	drawColoredQuad(x+3, y+3, PaddleWidth, PaddleHeight, 99, colorShadow())
	drawColoredQuad(x, y, PaddleWidth, PaddleHeight, 100, colorPaddle())
}

func renderBall() {
	x := float32(FieldX) + ballX
	y := float32(FieldY) + ballY
	drawColoredQuad(x, y, BallRadius*2, BallRadius*2, 100, colorWhite())
}

func renderHUD() {
	if fontTexture == nil {
		return
	}

	setupTexturedContext(kos.PVR_LIST_TR_POLY, fontTexture)

	hudX := float32(FieldX + FieldWidth + 15)
	white := colorWhite()
	gray := colorGray()

	drawString(hudX, 50, 200, white, "BRKOUT")

	drawString(hudX, 100, 200, gray, "Level")
	drawInteger(hudX, 125, 200, white, level)

	drawString(hudX, 170, 200, gray, "Score")
	drawInteger(hudX, 195, 200, white, score)

	drawString(hudX, 250, 200, gray, "Lives")
	drawInteger(hudX, 275, 200, white, lives)
}

// ----------------------------------------------------------------------------
// Main Entry Point
// ----------------------------------------------------------------------------

func main() {
	loadAssets()

	for {
		if showTitleMenu() {
			return // User selected Quit
		}
		runGameLoop()
	}
}