feat: add line clear animation
This commit is contained in:
parent
fdab0a1992
commit
db9ec53d35
4 changed files with 112 additions and 20 deletions
|
|
@ -64,7 +64,7 @@ go build tetris.go
|
|||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Line clear animations
|
||||
- [x] Line clear animations
|
||||
- [ ] Hard drop and ghost piece
|
||||
- [ ] Level/speed progression and pause
|
||||
- [ ] High score persistence
|
||||
|
|
|
|||
23
game/grid.go
23
game/grid.go
|
|
@ -1,6 +1,8 @@
|
|||
package game
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"tetris/engine/graphics"
|
||||
)
|
||||
|
||||
|
|
@ -44,6 +46,16 @@ func (g *Grid) ClearFullRows() byte {
|
|||
return completed
|
||||
}
|
||||
|
||||
func (g *Grid) FullRows() []byte {
|
||||
rows := []byte{}
|
||||
for y := byte(0); y < byte(g.Height()); y++ {
|
||||
if g.IsRowFull(y) {
|
||||
rows = append(rows, y)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (g *Grid) IsRowFull(y byte) bool {
|
||||
for x := range byte(g.Width()) {
|
||||
if g.At(x, y) == BLOCK_EMPTY {
|
||||
|
|
@ -53,6 +65,17 @@ func (g *Grid) IsRowFull(y byte) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (g *Grid) MoveRowsDown(rows ...byte) {
|
||||
completed := byte(0)
|
||||
for y := int(g.Height() - 1); y >= 0; y-- {
|
||||
if slices.Contains(rows, byte(y)) {
|
||||
completed++
|
||||
} else if completed > 0 {
|
||||
g.MoveRowDown(byte(y), completed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Grid) MoveRowDown(y, num_rows byte) {
|
||||
w := uint16(g.Width())
|
||||
src := uint16(y) * w
|
||||
|
|
|
|||
57
game/line_clear_animation.go
Normal file
57
game/line_clear_animation.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package game
|
||||
|
||||
// LineClearAnimation clears completed lines cell-by-cell to create
|
||||
// a visual wipe effect from the center outward.
|
||||
//
|
||||
// Once the lines are cleared, the remaining lines are moved down.
|
||||
type LineClearAnimation struct {
|
||||
// lines to clear.
|
||||
lines []byte
|
||||
// next index to clear to the left
|
||||
leftIndex int8
|
||||
// next index to clear to the right
|
||||
rightIndex int8
|
||||
}
|
||||
|
||||
func (lca *LineClearAnimation) Reset() {
|
||||
lca.lines = []byte{}
|
||||
}
|
||||
|
||||
func (lca LineClearAnimation) NumLines() int {
|
||||
return len(lca.lines)
|
||||
}
|
||||
|
||||
func (lca *LineClearAnimation) SetLines(rows []byte) {
|
||||
lca.lines = rows
|
||||
|
||||
lca.leftIndex = (GRID_WIDTH - 1) / 2
|
||||
if (GRID_WIDTH-1)%2 != 0 {
|
||||
lca.rightIndex = lca.leftIndex + 1
|
||||
} else {
|
||||
lca.rightIndex = lca.leftIndex
|
||||
}
|
||||
}
|
||||
|
||||
func (lca *LineClearAnimation) Update(grid *Grid) {
|
||||
if lca.Completed() {
|
||||
return
|
||||
}
|
||||
|
||||
if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH {
|
||||
grid.MoveRowsDown(lca.lines...)
|
||||
lca.Reset()
|
||||
return
|
||||
}
|
||||
|
||||
for _, lineIndex := range lca.lines {
|
||||
grid.Set(byte(lca.leftIndex), lineIndex, BLOCK_EMPTY)
|
||||
grid.Set(byte(lca.rightIndex), lineIndex, BLOCK_EMPTY)
|
||||
}
|
||||
|
||||
lca.leftIndex--
|
||||
lca.rightIndex++
|
||||
}
|
||||
|
||||
func (lca LineClearAnimation) Completed() bool {
|
||||
return lca.NumLines() == 0
|
||||
}
|
||||
|
|
@ -21,16 +21,19 @@ type GamePlay struct {
|
|||
score game.Score
|
||||
dropTimer core.IntervalTimer
|
||||
moveTimer core.IntervalTimer
|
||||
lineClearTimer core.IntervalTimer
|
||||
grid game.Grid
|
||||
nextShape game.Shape
|
||||
shapeQueue *game.ShapeQueue
|
||||
r draw.Renderer
|
||||
lineClearAnimation game.LineClearAnimation
|
||||
}
|
||||
|
||||
func NewGamePlay() *GamePlay {
|
||||
return &GamePlay{
|
||||
dropTimer: core.NewIntervalTimer(0.3),
|
||||
moveTimer: core.NewIntervalTimer(0.1),
|
||||
lineClearTimer: core.NewIntervalTimer(0.05),
|
||||
r: draw.Renderer{
|
||||
Theme: &draw.Theme{
|
||||
FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255},
|
||||
|
|
@ -50,6 +53,7 @@ func (gp *GamePlay) Enter() {
|
|||
gp.shapeQueue = game.NewShapeQueue()
|
||||
gp.nextShape = gp.shapeQueue.Next()
|
||||
gp.SpawnShape()
|
||||
gp.lineClearAnimation.Reset()
|
||||
}
|
||||
|
||||
func (GamePlay) Exit() {
|
||||
|
|
@ -81,6 +85,13 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
|||
gp.dropTimer.SetInterval(0.3)
|
||||
}
|
||||
|
||||
if !gp.lineClearAnimation.Completed() {
|
||||
if gp.lineClearTimer.UpdateReset(delta) {
|
||||
gp.lineClearAnimation.Update(&gp.grid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if rl.IsKeyPressed(rl.KeyUp) {
|
||||
rotated := gp.shape.RotateCW()
|
||||
if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) {
|
||||
|
|
@ -107,16 +118,17 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
|||
// Update position if it does not collide
|
||||
if game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
|
||||
gp.LockShape()
|
||||
num_rows := gp.grid.ClearFullRows()
|
||||
if num_rows > 0 {
|
||||
audio.Play(assets.SFX_ROW_CLEARED)
|
||||
gp.score.Lines(num_rows)
|
||||
}
|
||||
gp.SpawnShape()
|
||||
|
||||
lines := gp.grid.FullRows()
|
||||
if len(lines) > 0 {
|
||||
gp.lineClearAnimation.SetLines(lines)
|
||||
gp.score.Lines(byte(len(lines)))
|
||||
audio.Play(assets.SFX_ROW_CLEARED)
|
||||
} else {
|
||||
if game.CheckShapeCollision(gp.shape_pos, &gp.shape, &gp.grid) {
|
||||
fsm.Switch("gameover")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gp.shape_pos = new_pos
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue