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.
This commit is contained in:
Sander Schobers 2019-12-23 18:10:11 +01:00
parent 7bd737af94
commit 6cce47198b
8 changed files with 508 additions and 64 deletions

View File

@ -55,6 +55,8 @@ func (c *console) Render(ctx *alui.Context, bounds geom.RectangleF32) {
if messagesN != c.bufferN { if messagesN != c.bufferN {
c.buffer.SetAsTarget() c.buffer.SetAsTarget()
allg5.ClearToColor(allg5.NewColorAlpha(0, 0, 0, 0))
size := geom.PtF32(float32(size.X), float32(size.Y)) size := geom.PtF32(float32(size.X), float32(size.Y))
totalHeight := lineHeight * float32(messagesN) totalHeight := lineHeight * float32(messagesN)
if totalHeight < size.Y { if totalHeight < size.Y {

View File

@ -22,6 +22,7 @@ type Context struct {
Textures map[string]Texture Textures map[string]Texture
Levels map[string]level Levels map[string]level
Sprites map[string]sprite Sprites map[string]sprite
Settings Settings
Tick time.Duration Tick time.Duration
} }

View File

@ -130,9 +130,9 @@ func (g *Game) loadAssets() error {
log.Println("Loading textures...") log.Println("Loading textures...")
err := g.loadTextures(map[string]string{ err := g.loadTextures(map[string]string{
"basic_tile.png": "basic_tile", "basic_tile.png": "basic_tile",
"sunken_brick_tile.png": "sunken_brick_tile",
"water_tile.png": "water_tile", "water_tile.png": "water_tile",
// "dragon.png": "dragon",
"main_character.png": "main_character", "main_character.png": "main_character",
"villain_character.png": "villain_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 { func (g *Game) Init(disp *allg5.Display, res vfs.CopyDir, cons *gut.Console, fps *gut.FPS) error {
log.Print("Initializing game...") log.Print("Initializing game...")
g.ctx = &Context{Resources: res, Textures: map[string]Texture{}} g.ctx = &Context{Resources: res, Textures: map[string]Texture{}}
g.ctx.Settings = newDefaultSettings()
if err := g.initUI(disp, cons, fps); err != nil { if err := g.initUI(disp, cons, fps); err != nil {
return err return err
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"sort" "sort"
"time" "time"
@ -16,6 +17,7 @@ type playLevel struct {
ctx *Context ctx *Context
offset geom.PointF32 offset geom.PointF32
scale float32 scale float32
keysDown keyPressedState
level level level level
player *entity player *entity
@ -27,6 +29,18 @@ type playLevel struct {
ani gut.Animations 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 { type entity struct {
typ entityType typ entityType
pos geom.Point 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) isIdle() bool { return s.ani.Idle() }
func (s *playLevel) canMove(to geom.Point) bool { func findEntityAt(entities []*entity, pos geom.Point) *entity {
idx := s.level.posToIdx(to) idx := findEntityIdx(entities, pos)
if idx == -1 { 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 false
} }
return true 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) { func (s *playLevel) loadLevel(name string) {
s.keysDown = keyPressedState{}
s.level = s.ctx.Levels[name] s.level = s.ctx.Levels[name]
s.bricks = nil s.bricks = nil
s.sunken = nil s.sunken = nil
@ -126,40 +213,84 @@ func (s *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) {
s.ani.Animate(s.ctx.Tick) s.ani.Animate(s.ctx.Tick)
} }
func (s *playLevel) tryPlayerMove(to geom.Point) { func (s *playLevel) tryPlayerMove(dir geom.Point, key allg5.Key) {
if s.isIdle() && s.canMove(to) { if !s.isIdle() {
s.ani.Start(s.ctx.Tick, newEntityMoveAnimation(s.player, to)) 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) { func (s *playLevel) Handle(e allg5.Event) {
switch e := e.(type) { switch e := e.(type) {
case *allg5.KeyCharEvent: case *allg5.KeyDownEvent:
s.keysDown[e.KeyCode] = true
switch e.KeyCode { switch e.KeyCode {
case allg5.KeyUp: case s.ctx.Settings.Controls.MoveUp:
s.tryPlayerMove(s.player.pos.Add2D(0, -1)) s.tryPlayerMove(geom.Pt(0, -1), e.KeyCode)
case allg5.KeyRight: case s.ctx.Settings.Controls.MoveRight:
s.tryPlayerMove(s.player.pos.Add2D(1, 0)) s.tryPlayerMove(geom.Pt(1, 0), e.KeyCode)
case allg5.KeyDown: case s.ctx.Settings.Controls.MoveDown:
s.tryPlayerMove(s.player.pos.Add2D(0, 1)) s.tryPlayerMove(geom.Pt(0, 1), e.KeyCode)
case allg5.KeyLeft: case s.ctx.Settings.Controls.MoveLeft:
s.tryPlayerMove(s.player.pos.Add2D(-1, 0)) 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) { func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
basicTile := s.ctx.Textures["basic_tile"] basicTile := s.ctx.Textures["basic_tile"]
waterTile := s.ctx.Textures["water_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 { switch t {
case tileBasic: case tileBasic:
return basicTile basicTile.DrawOptions(scrPos.X, scrPos.Y, opts)
case tileWater: case tileWater:
return waterTile scrPos := scrPos.Add2D(0, 8*s.scale)
default: if findEntityAt(s.sunken, pos) == nil {
return Texture{} 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"] villain := s.ctx.Textures["villain_character"]
brick := s.ctx.Textures["brick"] brick := s.ctx.Textures["brick"]
crate := s.ctx.Textures["crate"] 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 var entities []*entity
entities = append(entities, s.player) entities = append(entities, s.player)
@ -213,11 +312,19 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
}) })
for _, e := range entities { for _, e := range entities {
bmp := entityBmp(e.typ)
if bmp.Bitmap == nil {
continue
}
scrPos := s.posToScreenF32(e.scr) 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)
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

29
cmd/krampus19/settings.go Normal file
View File

@ -0,0 +1,29 @@
package main
import "opslag.de/schobers/allg5"
type Settings struct {
Controls Controls
}
func newDefaultSettings() Settings {
return Settings{
Controls: Controls{
MoveUp: allg5.KeyUp,
MoveRight: allg5.KeyRight,
MoveDown: allg5.KeyDown,
MoveLeft: allg5.KeyLeft,
},
}
}
type Controls struct {
MoveUp allg5.Key
MoveRight allg5.Key
MoveDown allg5.Key
MoveLeft allg5.Key
}
func (c Controls) MovementKeys() []allg5.Key {
return []allg5.Key{c.MoveUp, c.MoveRight, c.MoveDown, c.MoveLeft}
}

View File

@ -6,6 +6,8 @@ type Animation interface {
Animate(start, now time.Duration) bool Animate(start, now time.Duration) bool
} }
type AnimationDoneFn func()
type Animations struct { type Animations struct {
anis []animation anis []animation
} }
@ -13,6 +15,7 @@ type Animations struct {
type animation struct { type animation struct {
start time.Duration start time.Duration
ani Animation ani Animation
done AnimationDoneFn
} }
func (a *Animations) Idle() bool { func (a *Animations) Idle() bool {
@ -20,15 +23,27 @@ func (a *Animations) Idle() bool {
} }
func (a *Animations) Start(now time.Duration, ani Animation) { func (a *Animations) Start(now time.Duration, ani Animation) {
a.anis = append(a.anis, animation{now, ani}) a.StartFn(now, ani, nil)
}
func (a *Animations) StartFn(now time.Duration, ani Animation, done AnimationDoneFn) {
a.anis = append(a.anis, animation{now, ani, done})
} }
func (a *Animations) Animate(now time.Duration) { func (a *Animations) Animate(now time.Duration) {
active := make([]animation, 0, len(a.anis)) active := make([]animation, 0, len(a.anis))
done := make([]animation, 0)
for _, ani := range a.anis { for _, ani := range a.anis {
if ani.ani.Animate(ani.start, now) { if ani.ani.Animate(ani.start, now) {
active = append(active, ani) active = append(active, ani)
} else {
done = append(done, ani)
} }
} }
a.anis = active a.anis = active
for _, ani := range done {
if ani.done != nil {
ani.done()
}
}
} }

289
gut/keys.go Normal file
View File

@ -0,0 +1,289 @@
package gut
import "opslag.de/schobers/allg5"
func KeyToString(k allg5.Key) string {
switch k {
case allg5.KeyA:
return "A"
case allg5.KeyB:
return "B"
case allg5.KeyC:
return "C"
case allg5.KeyD:
return "D"
case allg5.KeyE:
return "E"
case allg5.KeyF:
return "F"
case allg5.KeyG:
return "G"
case allg5.KeyH:
return "H"
case allg5.KeyI:
return "I"
case allg5.KeyJ:
return "J"
case allg5.KeyK:
return "K"
case allg5.KeyL:
return "L"
case allg5.KeyM:
return "M"
case allg5.KeyN:
return "N"
case allg5.KeyO:
return "O"
case allg5.KeyP:
return "P"
case allg5.KeyQ:
return "Q"
case allg5.KeyR:
return "R"
case allg5.KeyS:
return "S"
case allg5.KeyT:
return "T"
case allg5.KeyU:
return "U"
case allg5.KeyV:
return "V"
case allg5.KeyW:
return "W"
case allg5.KeyX:
return "X"
case allg5.KeyY:
return "Y"
case allg5.KeyZ:
return "Z"
case allg5.Key0:
return "0"
case allg5.Key1:
return "1"
case allg5.Key2:
return "2"
case allg5.Key3:
return "3"
case allg5.Key4:
return "4"
case allg5.Key5:
return "5"
case allg5.Key6:
return "6"
case allg5.Key7:
return "7"
case allg5.Key8:
return "8"
case allg5.Key9:
return "9"
case allg5.KeyPad0:
return "Pad 0"
case allg5.KeyPad1:
return "Pad 1"
case allg5.KeyPad2:
return "Pad 2"
case allg5.KeyPad3:
return "Pad 3"
case allg5.KeyPad4:
return "Pad 4"
case allg5.KeyPad5:
return "Pad 5"
case allg5.KeyPad6:
return "Pad 6"
case allg5.KeyPad7:
return "Pad 7"
case allg5.KeyPad8:
return "Pad 8"
case allg5.KeyPad9:
return "Pad 9"
case allg5.KeyF1:
return "F1"
case allg5.KeyF2:
return "F2"
case allg5.KeyF3:
return "F3"
case allg5.KeyF4:
return "F4"
case allg5.KeyF5:
return "F5"
case allg5.KeyF6:
return "F6"
case allg5.KeyF7:
return "F7"
case allg5.KeyF8:
return "F8"
case allg5.KeyF9:
return "F9"
case allg5.KeyF10:
return "F10"
case allg5.KeyF11:
return "F11"
case allg5.KeyF12:
return "F12"
case allg5.KeyEscape:
return "Escape"
case allg5.KeyTilde:
return "~"
case allg5.KeyMinus:
return "-"
case allg5.KeyEquals:
return "Equals"
case allg5.KeyBackspace:
return "Backspace"
case allg5.KeyTab:
return "Tab"
case allg5.KeyOpenBrace:
return "("
case allg5.KeyCloseBrace:
return ")"
case allg5.KeyEnter:
return "Enter"
case allg5.KeySemicolon:
return ";"
case allg5.KeyQuote:
return "'"
case allg5.KeyBackslash:
return "\\"
case allg5.KeyBackslash2:
return "\\"
case allg5.KeyComma:
return ","
case allg5.KeyFullstop:
return "."
case allg5.KeySlash:
return "/"
case allg5.KeySpace:
return "Space"
case allg5.KeyInsert:
return "Insert"
case allg5.KeyDelete:
return "Delete"
case allg5.KeyHome:
return "Home"
case allg5.KeyEnd:
return "End"
case allg5.KeyPageUp:
return "Page Up"
case allg5.KeyPageDown:
return "Page Down"
case allg5.KeyLeft:
return "Left"
case allg5.KeyRight:
return "Right"
case allg5.KeyUp:
return "Up"
case allg5.KeyDown:
return "Down"
case allg5.KeyPadSlash:
return "Slash"
case allg5.KeyPadAsterisk:
return "Asterisk"
case allg5.KeyPadMinus:
return "-"
case allg5.KeyPadPlus:
return "+"
case allg5.KeyPadDelete:
return "Delete"
case allg5.KeyPadEnter:
return "Enter"
case allg5.KeyPrintScreen:
return "Print Screen"
case allg5.KeyPause:
return "Pause"
case allg5.KeyAbntC1:
return "AbntC1"
case allg5.KeyYen:
return "Yen"
case allg5.KeyKana:
return "Kana"
case allg5.KeyConvert:
return "Convert"
case allg5.KeyNoConvert:
return "NoConvert"
case allg5.KeyAt:
return "@"
case allg5.KeyCircumflex:
return "^"
case allg5.KeyColon2:
return ":"
case allg5.KeyKanji:
return "Kanji"
case allg5.KeyPadEquals:
return "="
case allg5.KeyBackQuote:
return "`"
case allg5.KeySemicolon2:
return ";"
case allg5.KeyCommand:
return "Command"
case allg5.KeyBack:
return "Back"
case allg5.KeyVolumeUp:
return "Volume Up"
case allg5.KeyVolumeDown:
return "Volume Down"
case allg5.KeySearch:
return "Search"
case allg5.KeyDPadCenter:
return "D-pad Center"
case allg5.KeyButtonX:
return "Button X"
case allg5.KeyButtonY:
return "Button Y"
case allg5.KeyDPadUp:
return "D-pad Up"
case allg5.KeyDPadDown:
return "D-pad Down"
case allg5.KeyDPadLeft:
return "D-pad Left"
case allg5.KeyDPadRight:
return "D-pad Right"
case allg5.KeySelect:
return "Select"
case allg5.KeyStart:
return "Start"
case allg5.KeyButtonL1:
return "Button Left 1"
case allg5.KeyButtonR1:
return "Button Right 1"
case allg5.KeyButtonL2:
return "Button Left 2"
case allg5.KeyButtonR2:
return "Button Right 2"
case allg5.KeyButtonA:
return "Button A"
case allg5.KeyButtonB:
return "Button B"
case allg5.KeyThumbL:
return "Thumb Left"
case allg5.KeyThumbR:
return "Thumb Right"
case allg5.KeyUnknown:
return "Unknown"
case allg5.KeyLShift:
return "Left Shift"
case allg5.KeyRShift:
return "Right Shift"
case allg5.KeyLCtrl:
return "Left Control"
case allg5.KeyRCtrl:
return "Right Control"
case allg5.KeyAlt:
return "Alt"
case allg5.KeyAltGr:
return "AltGr"
case allg5.KeyLWin:
return "Left Windows"
case allg5.KeyRWin:
return "Right Windows"
case allg5.KeyMenu:
return "Menu"
case allg5.KeyScrollLock:
return "ScrollLock"
case allg5.KeyNumLock:
return "Numlock"
case allg5.KeyCapsLock:
return "Capslock"
}
return "Unknown"
}