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 0000000..2744900 Binary files /dev/null and b/cmd/krampus19/res/sunken_brick_tile.png differ diff --git a/cmd/krampus19/settings.go b/cmd/krampus19/settings.go new file mode 100644 index 0000000..97a5211 --- /dev/null +++ b/cmd/krampus19/settings.go @@ -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} +} diff --git a/gut/animation.go b/gut/animation.go index b4864bf..7adbf79 100644 --- a/gut/animation.go +++ b/gut/animation.go @@ -6,6 +6,8 @@ type Animation interface { Animate(start, now time.Duration) bool } +type AnimationDoneFn func() + type Animations struct { anis []animation } @@ -13,6 +15,7 @@ type Animations struct { type animation struct { start time.Duration ani Animation + done AnimationDoneFn } func (a *Animations) Idle() bool { @@ -20,15 +23,27 @@ func (a *Animations) Idle() bool { } 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) { active := make([]animation, 0, len(a.anis)) + done := make([]animation, 0) for _, ani := range a.anis { if ani.ani.Animate(ani.start, now) { active = append(active, ani) + } else { + done = append(done, ani) } } a.anis = active + for _, ani := range done { + if ani.done != nil { + ani.done() + } + } } diff --git a/gut/keys.go b/gut/keys.go new file mode 100644 index 0000000..8fb821a --- /dev/null +++ b/gut/keys.go @@ -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" +}