From 6cce47198b06f5b37bc55475f19815cd7e8bdf64 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Mon, 23 Dec 2019 18:10:11 +0100 Subject: [PATCH] Imroved movement - Player can only move to valid positions. - Bricks are pushed when this is allowed. - Bricks are sunken when they end up over water. Added settings (at runtime). Added new tile (sunken tile). Fixed bug with clearing of console. - When messages changed the new text was rendered on top of clearing the old text without clearing the buffer. --- cmd/krampus19/console.go | 2 + cmd/krampus19/context.go | 1 + cmd/krampus19/game.go | 7 +- cmd/krampus19/playlevel.go | 227 ++++++++++++++----- cmd/krampus19/res/sunken_brick_tile.png | Bin 0 -> 17246 bytes cmd/krampus19/settings.go | 29 +++ gut/animation.go | 17 +- gut/keys.go | 289 ++++++++++++++++++++++++ 8 files changed, 508 insertions(+), 64 deletions(-) create mode 100644 cmd/krampus19/res/sunken_brick_tile.png create mode 100644 cmd/krampus19/settings.go create mode 100644 gut/keys.go diff --git a/cmd/krampus19/console.go b/cmd/krampus19/console.go index 8fc79ca..2357989 100644 --- a/cmd/krampus19/console.go +++ b/cmd/krampus19/console.go @@ -55,6 +55,8 @@ func (c *console) Render(ctx *alui.Context, bounds geom.RectangleF32) { if messagesN != c.bufferN { c.buffer.SetAsTarget() + allg5.ClearToColor(allg5.NewColorAlpha(0, 0, 0, 0)) + size := geom.PtF32(float32(size.X), float32(size.Y)) totalHeight := lineHeight * float32(messagesN) if totalHeight < size.Y { diff --git a/cmd/krampus19/context.go b/cmd/krampus19/context.go index e57d4b5..6837d6d 100644 --- a/cmd/krampus19/context.go +++ b/cmd/krampus19/context.go @@ -22,6 +22,7 @@ type Context struct { Textures map[string]Texture Levels map[string]level Sprites map[string]sprite + Settings Settings Tick time.Duration } diff --git a/cmd/krampus19/game.go b/cmd/krampus19/game.go index 316c2a7..753ae8a 100644 --- a/cmd/krampus19/game.go +++ b/cmd/krampus19/game.go @@ -129,10 +129,10 @@ func (g *Game) loadSprites(names ...string) error { func (g *Game) loadAssets() error { log.Println("Loading textures...") err := g.loadTextures(map[string]string{ - "basic_tile.png": "basic_tile", - "water_tile.png": "water_tile", + "basic_tile.png": "basic_tile", + "sunken_brick_tile.png": "sunken_brick_tile", + "water_tile.png": "water_tile", - // "dragon.png": "dragon", "main_character.png": "main_character", "villain_character.png": "villain_character", @@ -174,6 +174,7 @@ func (g *Game) Destroy() { func (g *Game) Init(disp *allg5.Display, res vfs.CopyDir, cons *gut.Console, fps *gut.FPS) error { log.Print("Initializing game...") g.ctx = &Context{Resources: res, Textures: map[string]Texture{}} + g.ctx.Settings = newDefaultSettings() if err := g.initUI(disp, cons, fps); err != nil { return err } diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index a211925..9e6c087 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -1,6 +1,7 @@ package main import ( + "log" "sort" "time" @@ -13,9 +14,10 @@ import ( type playLevel struct { alui.ControlBase - ctx *Context - offset geom.PointF32 - scale float32 + ctx *Context + offset geom.PointF32 + scale float32 + keysDown keyPressedState level level player *entity @@ -27,6 +29,18 @@ type playLevel struct { ani gut.Animations } +type keyPressedState map[allg5.Key]bool + +func (s keyPressedState) CountPressed(keys ...allg5.Key) int { + var cnt int + for _, k := range keys { + if s[k] { + cnt++ + } + } + return cnt +} + type entity struct { typ entityType pos geom.Point @@ -68,15 +82,88 @@ func (s *playLevel) idxToPos(i int) geom.PointF32 { return s.level.idxToPos(i).T func (s *playLevel) isIdle() bool { return s.ani.Idle() } -func (s *playLevel) canMove(to geom.Point) bool { - idx := s.level.posToIdx(to) +func findEntityAt(entities []*entity, pos geom.Point) *entity { + idx := findEntityIdx(entities, pos) if idx == -1 { + return nil + } + return entities[idx] +} + +func findEntityIdx(entities []*entity, pos geom.Point) int { + for i, e := range entities { + if e.pos == pos { + return i + } + } + return -1 +} + +func (s *playLevel) findEntityAt(pos geom.Point) *entity { + if s.player.pos == pos { + return s.player + } + brick := findEntityAt(s.bricks, pos) + if brick != nil { + return brick + } + return findEntityAt(s.sunken, pos) +} + +func (s *playLevel) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool) bool { + return s.checkTileNotFound(pos, check, false) +} + +func (s *playLevel) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool, notFound bool) bool { + idx := s.level.posToIdx(pos) + if idx == -1 { + return notFound + } + return check(pos, idx, s.level.tiles[idx]) +} + +func (s *playLevel) isSolidTile(pos geom.Point, idx int, t tile) bool { + switch t { + case tileBasic: + return true + case tileWater: + return findEntityAt(s.sunken, pos) != nil + } + return false +} + +func (s *playLevel) wouldBrickSink(pos geom.Point, idx int, t tile) bool { + return t == tileWater && findEntityAt(s.sunken, pos) == nil +} + +func (s *playLevel) isObstructed(pos geom.Point, idx int, t tile) bool { + if findEntityAt(s.bricks, pos) != nil { + return true // brick + } + switch s.level.tiles[idx] { + case tileWater: + return findEntityAt(s.sunken, pos) != nil + case tileBasic: return false } return true } +func (s *playLevel) canMove(from, dir geom.Point) bool { + to := from.Add(dir) + if !s.checkTile(to, s.isSolidTile) { + return false + } + brick := findEntityAt(s.bricks, to) + if brick != nil { + brickTo := to.Add(dir) + return !s.checkTileNotFound(brickTo, s.isObstructed, true) + } + return true +} + func (s *playLevel) loadLevel(name string) { + s.keysDown = keyPressedState{} s.level = s.ctx.Levels[name] s.bricks = nil s.sunken = nil @@ -126,40 +213,84 @@ func (s *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) { s.ani.Animate(s.ctx.Tick) } -func (s *playLevel) tryPlayerMove(to geom.Point) { - if s.isIdle() && s.canMove(to) { - s.ani.Start(s.ctx.Tick, newEntityMoveAnimation(s.player, to)) +func (s *playLevel) tryPlayerMove(dir geom.Point, key allg5.Key) { + if !s.isIdle() { + return + } + + to := s.player.pos.Add(dir) + if !s.canMove(s.player.pos, dir) { + log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key)) + return + } + + log.Printf("Moving player to %s", to) + s.ani.StartFn(s.ctx.Tick, newEntityMoveAnimation(s.player, to), func() { + log.Println("Player movement finished") + if s.keysDown[key] && s.keysDown.CountPressed(s.ctx.Settings.Controls.MovementKeys()...) == 1 { + log.Printf("Key %s is still down, moving further", gut.KeyToString(key)) + s.tryPlayerMove(dir, key) + } + }) + + if brick := findEntityAt(s.bricks, to); brick != nil { + log.Printf("Pushing brick at %s", to) + brickTo := to.Add(dir) + s.ani.StartFn(s.ctx.Tick, newEntityMoveAnimation(brick, brickTo), func() { + log.Println("Brick movement finished") + if s.checkTile(brickTo, s.wouldBrickSink) { + log.Println("Sinking brick") + idx := findEntityIdx(s.bricks, brickTo) + s.bricks = append(s.bricks[:idx], s.bricks[idx+1:]...) + s.sunken = append(s.sunken, brick) + } + }) } } func (s *playLevel) Handle(e allg5.Event) { switch e := e.(type) { - case *allg5.KeyCharEvent: + case *allg5.KeyDownEvent: + s.keysDown[e.KeyCode] = true + switch e.KeyCode { - case allg5.KeyUp: - s.tryPlayerMove(s.player.pos.Add2D(0, -1)) - case allg5.KeyRight: - s.tryPlayerMove(s.player.pos.Add2D(1, 0)) - case allg5.KeyDown: - s.tryPlayerMove(s.player.pos.Add2D(0, 1)) - case allg5.KeyLeft: - s.tryPlayerMove(s.player.pos.Add2D(-1, 0)) + case s.ctx.Settings.Controls.MoveUp: + s.tryPlayerMove(geom.Pt(0, -1), e.KeyCode) + case s.ctx.Settings.Controls.MoveRight: + s.tryPlayerMove(geom.Pt(1, 0), e.KeyCode) + case s.ctx.Settings.Controls.MoveDown: + s.tryPlayerMove(geom.Pt(0, 1), e.KeyCode) + case s.ctx.Settings.Controls.MoveLeft: + s.tryPlayerMove(geom.Pt(-1, 0), e.KeyCode) } + case *allg5.KeyUpEvent: + s.keysDown[e.KeyCode] = false } } func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { - basicTile := s.ctx.Textures["basic_tile"] waterTile := s.ctx.Textures["water_tile"] - tileBmp := func(t tile) Texture { + sunkenBrickTile := s.ctx.Textures["sunken_brick_tile"] + + scale := 168 / float32(basicTile.Width()) + + // center := disp.Center() + opts := allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)} + level := s.level + for i, t := range level.tiles { + pos := geom.Pt(i%level.width, i/level.width) + scrPos := s.posToScreen(pos) switch t { case tileBasic: - return basicTile + basicTile.DrawOptions(scrPos.X, scrPos.Y, opts) case tileWater: - return waterTile - default: - return Texture{} + scrPos := scrPos.Add2D(0, 8*s.scale) + if findEntityAt(s.sunken, pos) == nil { + waterTile.DrawOptions(scrPos.X, scrPos.Y, opts) + } else { + sunkenBrickTile.DrawOptions(scrPos.X, scrPos.Y, opts) + } } } @@ -167,38 +298,6 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { villain := s.ctx.Textures["villain_character"] brick := s.ctx.Textures["brick"] crate := s.ctx.Textures["crate"] - entityBmp := func(e entityType) Texture { - switch e { - case entityTypeCharacter: - return character - case entityTypeVillain: - return villain - case entityTypeBrick: - return brick - case entityTypeCrate: - return crate - default: - return Texture{} - } - } - - scale := 168 / float32(basicTile.Width()) - - // center := disp.Center() - - level := s.level - for i, t := range level.tiles { - tile := tileBmp(t) - if tile.Bitmap == nil { - continue - } - pos := geom.Pt(i%level.width, i/level.width) - srcPos := s.posToScreen(pos) - if t == tileWater { - srcPos.Y += 8 * s.scale - } - tile.DrawOptions(srcPos.X, srcPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) - } var entities []*entity entities = append(entities, s.player) @@ -213,11 +312,19 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { }) for _, e := range entities { - bmp := entityBmp(e.typ) - if bmp.Bitmap == nil { - continue - } scrPos := s.posToScreenF32(e.scr) - bmp.DrawOptions(scrPos.X, scrPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) + switch e.typ { + case entityTypeCharacter: + character.DrawOptions(scrPos.X, scrPos.Y, opts) + case entityTypeVillain: + villain.DrawOptions(scrPos.X, scrPos.Y, opts) + case entityTypeBrick: + if findEntityAt(s.sunken, e.pos) != nil { + break + } + brick.DrawOptions(scrPos.X, scrPos.Y, opts) + case entityTypeCrate: + crate.DrawOptions(scrPos.X, scrPos.Y, opts) + } } } diff --git a/cmd/krampus19/res/sunken_brick_tile.png b/cmd/krampus19/res/sunken_brick_tile.png new file mode 100644 index 0000000000000000000000000000000000000000..2744900eb1dc3e0e92d650a822795a3150570bb8 GIT binary patch literal 17246 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