feat: add ghost piece
This commit is contained in:
parent
b6ec52a3d9
commit
cc8fa88afe
12 changed files with 162 additions and 11 deletions
|
|
@ -65,7 +65,8 @@ go build tetris.go
|
|||
## Roadmap
|
||||
|
||||
- [x] Line clear animations
|
||||
- [ ] Hard drop and ghost piece
|
||||
- [x] ghost piece
|
||||
- [ ] Hard drop
|
||||
- [ ] Level/speed progression and pause
|
||||
- [ ] High score persistence
|
||||
- [ ] Custom key bindings
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -16,12 +16,22 @@ const (
|
|||
BLOCK_ORANGE
|
||||
BLOCK_MAGENTA
|
||||
BLOCK_TEAL
|
||||
BLOCK_GHOST
|
||||
)
|
||||
|
||||
func (c Block) Tile() graphics.Tile {
|
||||
if c >= 8 {
|
||||
if c >= 9 {
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ 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
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ type Shape interface {
|
|||
}
|
||||
|
||||
func DrawShape(offset rl.Vector2, pos core.Vec2i8, shape Shape) {
|
||||
tile := shape.Tile()
|
||||
DrawShapeTile(offset, pos, shape, 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)
|
||||
|
|
|
|||
39
game/ghost_shape.go
Normal file
39
game/ghost_shape.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
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())
|
||||
}
|
||||
|
|
@ -32,15 +32,15 @@ func (lca *LineClearAnimation) SetLines(rows []byte) {
|
|||
}
|
||||
}
|
||||
|
||||
func (lca *LineClearAnimation) Update(grid *Grid) {
|
||||
func (lca *LineClearAnimation) Update(grid *Grid) bool {
|
||||
if lca.Completed() {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH {
|
||||
grid.MoveLinesDown(lca.lines...)
|
||||
lca.Reset()
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
for _, lineIndex := range lca.lines {
|
||||
|
|
@ -50,6 +50,8 @@ func (lca *LineClearAnimation) Update(grid *Grid) {
|
|||
|
||||
lca.leftIndex--
|
||||
lca.rightIndex++
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (lca LineClearAnimation) Completed() bool {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ 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 {
|
||||
|
|
@ -71,6 +75,10 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ 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
|
||||
|
|
@ -66,6 +68,7 @@ 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() {
|
||||
|
|
@ -90,7 +93,9 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
|||
|
||||
if !gp.lineClearAnimation.Completed() {
|
||||
if gp.lineClearTimer.UpdateReset(delta) {
|
||||
gp.lineClearAnimation.Update(&gp.grid)
|
||||
if gp.lineClearAnimation.Update(&gp.grid) {
|
||||
gp.ghost_needs_update = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -99,6 +104,7 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +117,7 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,12 +143,24 @@ 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(rl.NewVector2(25, 25), gp.grid)
|
||||
draw.DrawShape(rl.NewVector2(25, 25), gp.shape_pos, gp.shape)
|
||||
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.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))
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import (
|
|||
)
|
||||
|
||||
type OptionsMenu struct {
|
||||
sndVolume *widgets.Slider
|
||||
list *layouts.ListBox
|
||||
sndVolume *widgets.Slider
|
||||
ghostBlock *widgets.Checkbox
|
||||
list *layouts.ListBox
|
||||
}
|
||||
|
||||
func soundVolumeChanged(widget *widgets.Slider) {
|
||||
|
|
@ -33,10 +34,15 @@ 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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
62
game/ui/widgets/checkbox.go
Normal file
62
game/ui/widgets/checkbox.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue