|
|
|
@ -4,6 +4,7 @@ import ( |
|
|
|
|
"fmt" |
|
|
|
|
"image/color" |
|
|
|
|
"math/rand" |
|
|
|
|
"os" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"tetris/internal/font" |
|
|
|
@ -29,7 +30,11 @@ type Game struct { |
|
|
|
|
gameOver bool |
|
|
|
|
lastMoveDown time.Time |
|
|
|
|
lastRotate time.Time |
|
|
|
|
lastMoveLeft time.Time |
|
|
|
|
lastMoveRight time.Time |
|
|
|
|
fontRenderer *font.FontRenderer |
|
|
|
|
hardDropping bool |
|
|
|
|
hardDropCounter int |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// New creates a new game instance
|
|
|
|
@ -39,6 +44,8 @@ func New() *Game { |
|
|
|
|
dropInterval: config.InitialDropInterval, |
|
|
|
|
lastMoveDown: time.Now(), |
|
|
|
|
lastRotate: time.Now(), |
|
|
|
|
lastMoveLeft: time.Now(), |
|
|
|
|
lastMoveRight: time.Now(), |
|
|
|
|
fontRenderer: font.NewFontRenderer(), |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -53,7 +60,7 @@ func New() *Game { |
|
|
|
|
// Update handles game logic updates
|
|
|
|
|
func (g *Game) Update() error { |
|
|
|
|
if g.gameOver { |
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeySpace) { |
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyR) { |
|
|
|
|
*g = *New() |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
@ -114,13 +121,17 @@ func (g *Game) Draw(screen *ebiten.Image) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Draw actual piece
|
|
|
|
|
// Draw actual piece with special effect during hard drop
|
|
|
|
|
for _, pos := range g.currentPiece.GetAbsolutePositions() { |
|
|
|
|
if pos[1] >= 0 { |
|
|
|
|
if g.hardDropping { |
|
|
|
|
g.drawHardDropBlock(screen, pos[0], pos[1], g.currentPiece.BlockType) |
|
|
|
|
} else { |
|
|
|
|
g.drawBlock(screen, pos[0], pos[1], g.currentPiece.BlockType) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Draw next piece preview
|
|
|
|
|
if g.nextPiece != nil { |
|
|
|
@ -151,8 +162,8 @@ func (g *Game) Draw(screen *ebiten.Image) { |
|
|
|
|
|
|
|
|
|
if g.gameOver { |
|
|
|
|
text := i18n.GetText() |
|
|
|
|
g.fontRenderer.DrawText(screen, text.GameOver, float64(config.PreviewX-config.BlockSize), float64(config.ScreenHeight/2), color.RGBA{255, 255, 255, 255}) |
|
|
|
|
g.fontRenderer.DrawText(screen, text.Restart, float64(config.PreviewX-config.BlockSize), float64(config.ScreenHeight/2+20), color.RGBA{255, 255, 255, 255}) |
|
|
|
|
g.fontRenderer.DrawText(screen, text.GameOver, float64(config.ScreenWidth/2-20), float64(config.ScreenHeight/2-100), color.RGBA{255, 255, 255, 255}) |
|
|
|
|
g.fontRenderer.DrawText(screen, text.Restart, float64(config.ScreenWidth/2-20), float64(config.ScreenHeight/2-80), color.RGBA{255, 0, 0, 255}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -168,8 +179,8 @@ func (g *Game) handleInput() { |
|
|
|
|
i18n.SwitchLanguage() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Restart game when game over
|
|
|
|
|
if g.gameOver && inpututil.IsKeyJustPressed(ebiten.KeyR) { |
|
|
|
|
// Restart game anytime
|
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyR) { |
|
|
|
|
g.restart() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
@ -179,12 +190,37 @@ func (g *Game) handleInput() { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Move left/right
|
|
|
|
|
// Hard drop
|
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeySpace) { |
|
|
|
|
g.hardDrop() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Skip movement inputs during hard drop animation
|
|
|
|
|
if g.hardDropping { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Move left/right - support continuous movement when holding keys
|
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyLeft) { |
|
|
|
|
// First press moves immediately, then add delay for continuous movement
|
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyLeft) { |
|
|
|
|
g.tryMove(-1, 0) |
|
|
|
|
g.lastMoveLeft = time.Now() |
|
|
|
|
} else if time.Since(g.lastMoveLeft) > time.Millisecond*120 { |
|
|
|
|
g.tryMove(-1, 0) |
|
|
|
|
g.lastMoveLeft = time.Now() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyRight) { |
|
|
|
|
// First press moves immediately, then add delay for continuous movement
|
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyRight) { |
|
|
|
|
g.tryMove(1, 0) |
|
|
|
|
g.lastMoveRight = time.Now() |
|
|
|
|
} else if time.Since(g.lastMoveRight) > time.Millisecond*120 { |
|
|
|
|
g.tryMove(1, 0) |
|
|
|
|
g.lastMoveRight = time.Now() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Rotate
|
|
|
|
@ -202,15 +238,26 @@ func (g *Game) handleInput() { |
|
|
|
|
g.lastMoveDown = time.Now() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Hard drop
|
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeySpace) { |
|
|
|
|
g.hardDrop() |
|
|
|
|
// Exit game
|
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { |
|
|
|
|
os.Exit(0) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// updateDropCounter handles automatic piece dropping
|
|
|
|
|
// updateDropCounter handles automatic piece dropping and hard drop animation
|
|
|
|
|
func (g *Game) updateDropCounter() { |
|
|
|
|
if g.hardDropping { |
|
|
|
|
// Hard drop animation - move faster
|
|
|
|
|
g.hardDropCounter++ |
|
|
|
|
if g.hardDropCounter >= 2 { // Move every 2 frames for visible animation
|
|
|
|
|
g.hardDropCounter = 0 |
|
|
|
|
if !g.tryMove(0, 1) { |
|
|
|
|
g.hardDropping = false |
|
|
|
|
g.lockPiece() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// Normal drop
|
|
|
|
|
g.dropCounter++ |
|
|
|
|
if g.dropCounter >= g.dropInterval { |
|
|
|
|
g.dropCounter = 0 |
|
|
|
@ -218,6 +265,7 @@ func (g *Game) updateDropCounter() { |
|
|
|
|
g.lockPiece() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// tryMove attempts to move the current piece and returns success
|
|
|
|
@ -275,24 +323,14 @@ func (g *Game) tryRotate() bool { |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// hardDrop drops the piece instantly to the bottom
|
|
|
|
|
// hardDrop starts the hard drop animation
|
|
|
|
|
func (g *Game) hardDrop() { |
|
|
|
|
if g.currentPiece == nil { |
|
|
|
|
if g.currentPiece == nil || g.hardDropping { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
dropDistance := 0 |
|
|
|
|
for { |
|
|
|
|
if !g.tryMove(0, 1) { |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
dropDistance++ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Award extra points for hard drop
|
|
|
|
|
g.score += dropDistance * 2 |
|
|
|
|
|
|
|
|
|
g.lockPiece() |
|
|
|
|
g.hardDropping = true |
|
|
|
|
g.hardDropCounter = 0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// isColliding checks if the current piece is colliding with the board or boundaries
|
|
|
|
@ -454,6 +492,40 @@ func (g *Game) drawBlock(screen *ebiten.Image, x, y int, blockType types.BlockTy |
|
|
|
|
borderColor, false) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// drawHardDropBlock draws a block with special hard drop effect
|
|
|
|
|
func (g *Game) drawHardDropBlock(screen *ebiten.Image, x, y int, blockType types.BlockType) { |
|
|
|
|
// Get original color and make it brighter for hard drop effect
|
|
|
|
|
originalColor := types.BlockColors[blockType].(color.RGBA) |
|
|
|
|
hardDropColor := color.RGBA{ |
|
|
|
|
min(255, originalColor.R+50), |
|
|
|
|
min(255, originalColor.G+50), |
|
|
|
|
min(255, originalColor.B+50), |
|
|
|
|
255, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
vector.DrawFilledRect(screen, |
|
|
|
|
float32(x*config.BlockSize), |
|
|
|
|
float32(y*config.BlockSize), |
|
|
|
|
float32(config.BlockSize-1), |
|
|
|
|
float32(config.BlockSize-1), |
|
|
|
|
hardDropColor, false) |
|
|
|
|
|
|
|
|
|
// Add bright white border for hard drop effect
|
|
|
|
|
borderColor := color.RGBA{255, 255, 255, 200} |
|
|
|
|
vector.DrawFilledRect(screen, |
|
|
|
|
float32(x*config.BlockSize), |
|
|
|
|
float32(y*config.BlockSize), |
|
|
|
|
float32(config.BlockSize-1), |
|
|
|
|
3, |
|
|
|
|
borderColor, false) |
|
|
|
|
vector.DrawFilledRect(screen, |
|
|
|
|
float32(x*config.BlockSize), |
|
|
|
|
float32(y*config.BlockSize), |
|
|
|
|
3, |
|
|
|
|
float32(config.BlockSize-1), |
|
|
|
|
borderColor, false) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// max returns the maximum of two integers
|
|
|
|
|
func max(a, b int) int { |
|
|
|
|
if a > b { |
|
|
|
@ -462,6 +534,14 @@ func max(a, b int) int { |
|
|
|
|
return b |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// min returns the minimum of two uint8 values
|
|
|
|
|
func min(a, b uint8) uint8 { |
|
|
|
|
if a < b { |
|
|
|
|
return a |
|
|
|
|
} |
|
|
|
|
return b |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// drawControls renders the control instructions on the right side of the screen
|
|
|
|
|
func (g *Game) drawControls(screen *ebiten.Image) { |
|
|
|
|
// Starting position for controls display
|
|
|
|
|