diff --git a/README.md b/README.md index f7efed0..a1ff326 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,7 @@ go build tetris.go ## Roadmap - [x] Line clear animations -- [x] ghost piece -- [ ] Hard drop +- [ ] Hard drop and ghost piece - [ ] Level/speed progression and pause - [ ] High score persistence - [ ] Custom key bindings diff --git a/assets/sprite.xcf b/assets/sprite.xcf index e376963..982b272 100644 Binary files a/assets/sprite.xcf and b/assets/sprite.xcf differ diff --git a/assets/sprites.png b/assets/sprites.png index b26a3b6..4b350bd 100644 Binary files a/assets/sprites.png and b/assets/sprites.png differ diff --git a/engine/graphics/image.go b/engine/graphics/image.go index 26ef9cb..d92b93c 100644 --- a/engine/graphics/image.go +++ b/engine/graphics/image.go @@ -1,7 +1,7 @@ -package graphics - -import rl "github.com/gen2brain/raylib-go/raylib" - -func LoadImageFromMemory(typ string, data []byte) *rl.Image { - return rl.LoadImageFromMemory(typ, data, int32(len(data))) -} +package graphics + +import rl "github.com/gen2brain/raylib-go/raylib" + +func LoadImageFromMemory(typ string, data []byte) *rl.Image { + return rl.LoadImageFromMemory(typ, data, int32(len(data))) +} diff --git a/game/block.go b/game/block.go index 5b7445a..0221d49 100644 --- a/game/block.go +++ b/game/block.go @@ -16,22 +16,12 @@ const ( BLOCK_ORANGE BLOCK_MAGENTA BLOCK_TEAL - BLOCK_GHOST ) func (c Block) Tile() graphics.Tile { - if c >= 9 { + if c >= 8 { c = BLOCK_EMPTY } - - if c == BLOCK_GHOST { - return graphics.Tile{ - Size: assets.BLOCK_TILE_SIZE, - X: byte(7), - Y: byte(1), - } - } - return graphics.Tile{ Size: assets.BLOCK_TILE_SIZE, X: byte(c), diff --git a/game/config/config.go b/game/config/config.go index 7437eae..82ad3d8 100644 --- a/game/config/config.go +++ b/game/config/config.go @@ -9,13 +9,11 @@ import ( type Config struct { fd *os.File SoundVolume byte `json:"sound_volume"` - GhostPiece bool `json:"ghost_piece"` } func DefaultConfig(fd *os.File) *Config { cfg := Config{ SoundVolume: 255, - GhostPiece: false, } cfg.fd = fd return &cfg diff --git a/game/draw/shape.go b/game/draw/shape.go index 08847df..8913b41 100644 --- a/game/draw/shape.go +++ b/game/draw/shape.go @@ -14,10 +14,8 @@ type Shape interface { } func DrawShape(offset rl.Vector2, pos core.Vec2i8, shape Shape) { - DrawShapeTile(offset, pos, shape, shape.Tile()) -} + tile := shape.Tile() -func DrawShapeTile(offset rl.Vector2, pos core.Vec2i8, shape Shape, tile graphics.Tile) { for _, coord := range shape.Coordinates() { block := pos.Add(coord) grid.DrawBlock(offset, byte(block.X), byte(block.Y), tile) diff --git a/game/ghost_shape.go b/game/ghost_shape.go deleted file mode 100644 index 1a2a96e..0000000 --- a/game/ghost_shape.go +++ /dev/null @@ -1,39 +0,0 @@ -package game - -import ( - "tetris/engine/core" - "tetris/game/draw" - - rl "github.com/gen2brain/raylib-go/raylib" -) - -type GhostShape struct { - posY int8 // only need Y coordinate -} - -func (gs *GhostShape) UpdatePos(shape *Shape, pos core.Vec2i8, grid *Grid) { - for gs.posY = pos.Y; pos.Y <= int8(grid.Height()); pos.Y++ { - if CheckShapeCollision(pos, shape, grid) { - gs.posY = pos.Y - 1 - break - } - } -} - -func (gs *GhostShape) Draw(offset rl.Vector2, shape *Shape, pos core.Vec2i8) { - draw.DrawShapeTile(offset, core.NewVec2(pos.X, gs.posY), shape, BLOCK_GHOST.Tile()) -} - -func UpdateGhostShapeY(shape *Shape, pos core.Vec2i8, grid *Grid) int8 { - for ; pos.Y <= int8(grid.Height()); pos.Y++ { - if CheckShapeCollision(pos, shape, grid) { - pos.Y = pos.Y - 1 - break - } - } - return pos.Y -} - -func DrawGhost(offset rl.Vector2, shape *Shape, pos core.Vec2i8, ghost_y int8) { - draw.DrawShapeTile(offset, core.NewVec2(pos.X, ghost_y), shape, BLOCK_GHOST.Tile()) -} diff --git a/game/line_clear_animation.go b/game/line_clear_animation.go index d79ff8a..5c650d5 100644 --- a/game/line_clear_animation.go +++ b/game/line_clear_animation.go @@ -32,15 +32,15 @@ func (lca *LineClearAnimation) SetLines(rows []byte) { } } -func (lca *LineClearAnimation) Update(grid *Grid) bool { +func (lca *LineClearAnimation) Update(grid *Grid) { if lca.Completed() { - return false + return } if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH { grid.MoveLinesDown(lca.lines...) lca.Reset() - return true + return } for _, lineIndex := range lca.lines { @@ -50,8 +50,6 @@ func (lca *LineClearAnimation) Update(grid *Grid) bool { lca.leftIndex-- lca.rightIndex++ - - return false } func (lca LineClearAnimation) Completed() bool { diff --git a/game/score.go b/game/score.go index d958059..ac00af7 100644 --- a/game/score.go +++ b/game/score.go @@ -5,7 +5,7 @@ package game const SCORE_MAX Score = 9999999 // Score is a running total of points earned. -// It's a distinct type (over uint32) to prevent mixing raw integers with scores. +// It’s a distinct type (over uint32) to prevent mixing raw integers with scores. type Score uint32 // Lines adds points for clearing a number of lines at once. diff --git a/game/shape.go b/game/shape.go index c6cccfd..7543e39 100644 --- a/game/shape.go +++ b/game/shape.go @@ -58,10 +58,6 @@ func (s Shape) Coordinates() [4]core.Vec2i8 { } func (s Shape) RotateCW() Shape { - if s.typ == SHAPE_O { - return s - } - rotated := [3]core.Vec2i8{} for i, block := range s.coordinates { @@ -75,10 +71,6 @@ func (s Shape) RotateCW() Shape { } func (s Shape) RotateCCW() Shape { - if s.typ == SHAPE_O { - return s - } - rotated := [3]core.Vec2i8{} for i, block := range s.coordinates { diff --git a/game/state/handlers/gameplay.go b/game/state/handlers/gameplay.go index 7730635..b65da11 100644 --- a/game/state/handlers/gameplay.go +++ b/game/state/handlers/gameplay.go @@ -18,8 +18,6 @@ import ( type GamePlay struct { shape game.Shape shape_pos core.Vec2i8 - ghost game.GhostShape - ghost_needs_update bool score game.Score dropTimer core.IntervalTimer moveTimer core.IntervalTimer @@ -68,7 +66,6 @@ func (gp *GamePlay) SpawnShape() { gp.shape = gp.nextShape gp.nextShape = gp.shapeQueue.Next() gp.shape_pos = core.Vec2i8{X: 4, Y: 0} - gp.ghost_needs_update = true } func (gp *GamePlay) LockShape() { @@ -93,9 +90,7 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { if !gp.lineClearAnimation.Completed() { if gp.lineClearTimer.UpdateReset(delta) { - if gp.lineClearAnimation.Update(&gp.grid) { - gp.ghost_needs_update = true - } + gp.lineClearAnimation.Update(&gp.grid) } return } @@ -104,7 +99,6 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { rotated := gp.shape.RotateCW() if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) { gp.shape = rotated - gp.ghost_needs_update = true } } @@ -117,7 +111,6 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { } if !game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) { gp.shape_pos.X = new_pos.X - gp.ghost_needs_update = true } } @@ -143,24 +136,12 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { gp.shape_pos = new_pos } } - - if gp.ghost_needs_update { - if game.Config.GhostPiece { - gp.ghost.UpdatePos(&gp.shape, gp.shape_pos, &gp.grid) - } - gp.ghost_needs_update = false - } } func (gp GamePlay) Render() { - offset := rl.NewVector2(25, 25) - render.Begin(gp.r.Theme.GridBackground) - gp.r.DrawGrid(offset, gp.grid) - draw.DrawShape(offset, gp.shape_pos, gp.shape) - if game.Config.GhostPiece && gp.lineClearAnimation.Completed() { - gp.ghost.Draw(offset, &gp.shape, gp.shape_pos) - } + gp.r.DrawGrid(rl.NewVector2(25, 25), gp.grid) + draw.DrawShape(rl.NewVector2(25, 25), gp.shape_pos, gp.shape) gp.r.DrawFrame(rl.RectangleInt32{X: 400, Y: 25, Width: 250, Height: 100}) gp.r.DrawHeaderText(410, 30, "Score") gp.r.DrawText(410, 65, fmt.Sprintf("%.7d", gp.score)) diff --git a/game/state/handlers/options.go b/game/state/handlers/options.go index 9456389..8fabd6d 100644 --- a/game/state/handlers/options.go +++ b/game/state/handlers/options.go @@ -16,9 +16,8 @@ import ( ) type OptionsMenu struct { - sndVolume *widgets.Slider - ghostBlock *widgets.Checkbox - list *layouts.ListBox + sndVolume *widgets.Slider + list *layouts.ListBox } func soundVolumeChanged(widget *widgets.Slider) { @@ -34,15 +33,10 @@ func soundVolumeChanged(widget *widgets.Slider) { func NewOptionsMenu() *OptionsMenu { sndVolume := widgets.NewSlider(0, 255, 10).WithOnChange(soundVolumeChanged) - ghostPiece := widgets.NewCheckbox(game.Config.GhostPiece, func(value bool) { - game.Config.GhostPiece = value - game.Config.Save() - }) return &OptionsMenu{ sndVolume: sndVolume, list: layouts.NewListBox([]ui.InputWidget{ layouts.NewInputControl(widgets.NewLabel("Sound Volume", 16), sndVolume), - layouts.NewInputControl(widgets.NewLabel("Show ghost piece", 16), ghostPiece), }).Spacing(10).OnSelect(uievents.MenuSelect), } } diff --git a/game/ui/widgets/checkbox.go b/game/ui/widgets/checkbox.go deleted file mode 100644 index 5954c25..0000000 --- a/game/ui/widgets/checkbox.go +++ /dev/null @@ -1,62 +0,0 @@ -package widgets - -import ( - "tetris/engine/core" - "tetris/engine/input" - "tetris/engine/render" - "tetris/game/ui/base" - - rl "github.com/gen2brain/raylib-go/raylib" -) - -type CheckboxAction func(value bool) - -type Checkbox struct { - *base.WithFocus - action CheckboxAction - value bool -} - -func NewCheckbox(value bool, action CheckboxAction) *Checkbox { - return &Checkbox{ - WithFocus: &base.WithFocus{}, - value: value, - action: action, - } -} - -func (cb Checkbox) Size() core.Vec2i { - return core.NewVec2(24, 24) -} - -func (cb *Checkbox) HandleInput() { - if input.KeyPressedWithRepeat(rl.KeyEnter) { - cb.value = !cb.value - cb.action(cb.value) - } -} - -func (cb Checkbox) Draw(x, y int32) { - size := cb.Size() - space := int32(2) - color := rl.White - if cb.HasFocus() { - color = rl.Red - } - - render.DrawRectOutlineBorder(rl.RectangleInt32{ - X: x, - Y: y, - Width: int32(size.X), - Height: int32(size.Y), - }, 4, color) - - if cb.value { - render.DrawRectBorder(rl.RectangleInt32{ - X: x + space, - Y: y + space, - Width: int32(size.X) - (space * 2), - Height: int32(size.Y) - (space * 2), - }, color, 0, rl.Blank) - } -}