Added simple main menu.

Refactored some structs from public to internal.
Separated navigation from game.
Added utility methods for drawing text.
Stackpanel will uses all available width when layouting.
This commit is contained in:
Sander Schobers 2019-12-24 01:27:05 +01:00
parent a6cb582254
commit 0cd5cb4ad1
13 changed files with 282 additions and 165 deletions

View File

@ -11,21 +11,21 @@ type Button struct {
ControlBase ControlBase
Text string Text string
TextAlign allg5.HorizontalAlignment
} }
func (b *Button) DesiredSize(ctx *Context) geom.PointF32 { func (b *Button) DesiredSize(ctx *Context) geom.PointF32 {
font := ctx.Fonts.Get(b.Font) font := ctx.Fonts.Get(b.Font)
_, _, w, h := font.TextDimensions(b.Text) w := font.TextWidth(b.Text)
return geom.PtF32(w+8, h+8) return geom.PtF32(w+8, font.Height()+8)
} }
func (b *Button) Render(ctx *Context, bounds geom.RectangleF32) { func (b *Button) Render(ctx *Context, bounds geom.RectangleF32) {
font := ctx.Fonts.Get(b.Font)
fore := ctx.Palette.Primary fore := ctx.Palette.Primary
if b.Over { if b.Over {
fore = ctx.Palette.Dark fore = ctx.Palette.Dark
ctx.Cursor = allg5.MouseCursorLink ctx.Cursor = allg5.MouseCursorLink
} }
font.Draw(bounds.Min.X+4, bounds.Min.Y+4, fore, allg5.AlignLeft, b.Text) font := ctx.Fonts.Get(b.Font)
ctx.Fonts.DrawAlignFont(font, bounds.Min.X+4, bounds.Min.Y+4, bounds.Max.X-4, fore, b.TextAlign, b.Text)
} }

View File

@ -2,6 +2,7 @@ package alui
import ( import (
"opslag.de/schobers/allg5" "opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
) )
type Fonts struct { type Fonts struct {
@ -25,6 +26,41 @@ func (f *Fonts) Destroy() {
f.fonts = nil f.fonts = nil
} }
func (f *Fonts) Draw(font string, left, top float32, color allg5.Color, text string) {
f.DrawFont(f.Get(font), left, top, color, text)
}
func (f *Fonts) DrawAlign(font string, left, top, right float32, color allg5.Color, align allg5.HorizontalAlignment, text string) {
f.DrawAlignFont(f.Get(font), left, top, right, color, align, text)
}
func (f *Fonts) DrawAlignFont(font *allg5.Font, left, top, right float32, color allg5.Color, align allg5.HorizontalAlignment, text string) {
switch align {
case allg5.AlignCenter:
center, top := geom.Round32(.5*(left+right)), geom.Round32(top)
font.Draw(center, top, color, allg5.AlignCenter, text)
case allg5.AlignRight:
right, top = geom.Round32(right), geom.Round32(top)
font.Draw(right, top, color, allg5.AlignRight, text)
default:
left, top = geom.Round32(left), geom.Round32(top)
font.Draw(left, top, color, allg5.AlignRight, text)
}
}
func (f *Fonts) DrawCenter(font string, center, top float32, color allg5.Color, text string) {
f.DrawCenterFont(f.Get(font), center, top, color, text)
}
func (f *Fonts) DrawCenterFont(font *allg5.Font, center, top float32, color allg5.Color, text string) {
f.DrawAlignFont(font, center, top, center, color, allg5.AlignCenter, text)
}
func (f *Fonts) DrawFont(font *allg5.Font, left, top float32, color allg5.Color, text string) {
left, top = geom.Round32(left), geom.Round32(top)
font.Draw(left, top, color, allg5.AlignLeft, text)
}
func (f *Fonts) Len() int { return len(f.fonts) } func (f *Fonts) Len() int { return len(f.fonts) }
func (f *Fonts) Load(path string, size int, name string) error { func (f *Fonts) Load(path string, size int, name string) error {

View File

@ -20,8 +20,6 @@ func (l *Label) DesiredSize(ctx *Context) geom.PointF32 {
} }
func (l *Label) Render(ctx *Context, bounds geom.RectangleF32) { func (l *Label) Render(ctx *Context, bounds geom.RectangleF32) {
font := ctx.Fonts.Get(l.Font)
back := l.Background back := l.Background
fore := l.Foreground fore := l.Foreground
if fore == nil { if fore == nil {
@ -31,5 +29,5 @@ func (l *Label) Render(ctx *Context, bounds geom.RectangleF32) {
if back != nil { if back != nil {
allg5.DrawFilledRectangle(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, *back) allg5.DrawFilledRectangle(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, *back)
} }
font.Draw(bounds.Min.X+4, bounds.Min.Y+4, *fore, allg5.AlignLeft, l.Text) ctx.Fonts.Draw(l.Font, bounds.Min.X+4, bounds.Min.Y+4, *fore, l.Text)
} }

View File

@ -78,8 +78,8 @@ func (s *StackPanel) DesiredSize(ctx *Context) geom.PointF32 {
func (s *StackPanel) Layout(ctx *Context, bounds geom.RectangleF32) { func (s *StackPanel) Layout(ctx *Context, bounds geom.RectangleF32) {
s.Container.Layout(ctx, bounds) s.Container.Layout(ctx, bounds)
desired, size := s.CalculateLayout(ctx) desired, _ := s.CalculateLayout(ctx)
width := s.asWidth(size) width := s.asWidth(bounds.Size())
var offset = bounds.Min var offset = bounds.Min
for i, child := range s.Children { for i, child := range s.Children {

View File

@ -27,7 +27,6 @@ func newConsole(cons *gut.Console) *console {
func (c *console) Render(ctx *alui.Context, bounds geom.RectangleF32) { func (c *console) Render(ctx *alui.Context, bounds geom.RectangleF32) {
back := allg5.NewColorAlpha(0, 0, 0, 0x7f) back := allg5.NewColorAlpha(0, 0, 0, 0x7f)
line := allg5.NewColor(0x7f, 0x7f, 0x7f) line := allg5.NewColor(0x7f, 0x7f, 0x7f)
fore := allg5.NewColor(0xff, 0xff, 0xff)
c.height = geom.Min32(c.height, bounds.Dy()) c.height = geom.Min32(c.height, bounds.Dy())
top := geom.Max32(bounds.Max.Y-c.height, bounds.Min.Y) top := geom.Max32(bounds.Max.Y-c.height, bounds.Min.Y)
@ -65,7 +64,7 @@ func (c *console) Render(ctx *alui.Context, bounds geom.RectangleF32) {
messageTop := size.Y - totalHeight - c.offset messageTop := size.Y - totalHeight - c.offset
for _, m := range messages { for _, m := range messages {
if messageTop <= size.Y || (messageTop+lineHeight) >= 0 { if messageTop <= size.Y || (messageTop+lineHeight) >= 0 {
font.Draw(0, messageTop, fore, allg5.AlignLeft, m) ctx.Fonts.DrawFont(font, 0, messageTop, ctx.Palette.Text, m)
} }
messageTop += lineHeight messageTop += lineHeight
} }

View File

@ -3,28 +3,32 @@ package main
import ( import (
"time" "time"
"opslag.de/schobers/krampus19/alui"
"opslag.de/schobers/allg5" "opslag.de/schobers/allg5"
"opslag.de/schobers/fs/vfs" "opslag.de/schobers/fs/vfs"
) )
type Texture struct { type texture struct {
*allg5.Bitmap *allg5.Bitmap
Subs map[string]*allg5.Bitmap Subs map[string]*allg5.Bitmap
} }
func NewTexture(bmp *allg5.Bitmap) Texture { func newTexture(bmp *allg5.Bitmap) texture {
return Texture{Bitmap: bmp, Subs: map[string]*allg5.Bitmap{}} return texture{Bitmap: bmp, Subs: map[string]*allg5.Bitmap{}}
} }
type Context struct { type Context struct {
Resources vfs.CopyDir Resources vfs.CopyDir
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 Settings settings
Palette *alui.Palette
Tick time.Duration Tick time.Duration
Navigation navigation
} }
func (c *Context) Destroy() { func (c *Context) Destroy() {

View File

@ -12,7 +12,7 @@ import (
"opslag.de/schobers/krampus19/gut" "opslag.de/schobers/krampus19/gut"
) )
type Game struct { type game struct {
ctx *Context ctx *Context
ui *alui.UI ui *alui.UI
main alui.Container main alui.Container
@ -22,10 +22,17 @@ type Game struct {
scene *alui.Overlay scene *alui.Overlay
} }
func (g *Game) initUI(disp *allg5.Display, cons *gut.Console, fps *gut.FPS) error { func (g *game) initUI(disp *allg5.Display, cons *gut.Console, fps *gut.FPS) error {
disp.SetWindowTitle("Krampushack '19 - Title TBD - by Tharro") disp.SetWindowTitle("Krampushack '19 - Title TBD - by Tharro")
ui := alui.NewUI(disp, &g.main) ui := alui.NewUI(disp, &g.main)
ui.SetPalette(alui.Palette{
Icon: allg5.NewColor(0x21, 0x21, 0x21),
Dark: allg5.NewColor(0xf7, 0xd1, 0x56),
Primary: allg5.NewColor(0xf7, 0xbd, 0x00),
Text: allg5.NewColor(0xff, 0xff, 0xff),
})
g.ui = ui g.ui = ui
g.ctx.Palette = &g.ui.Context().Palette
g.info = &alui.Overlay{Proxy: &info{fps: fps}} g.info = &alui.Overlay{Proxy: &info{fps: fps}}
g.cons = &alui.Overlay{Proxy: newConsole(cons)} g.cons = &alui.Overlay{Proxy: newConsole(cons)}
g.scene = &alui.Overlay{Proxy: &splash{}, Visible: true} g.scene = &alui.Overlay{Proxy: &splash{}, Visible: true}
@ -33,7 +40,7 @@ func (g *Game) initUI(disp *allg5.Display, cons *gut.Console, fps *gut.FPS) erro
return nil return nil
} }
func (g *Game) loadBitmap(path, name string) error { func (g *game) loadBitmap(path, name string) error {
f, err := g.ctx.Resources.Open(path) f, err := g.ctx.Resources.Open(path)
if err != nil { if err != nil {
return err return err
@ -47,11 +54,11 @@ func (g *Game) loadBitmap(path, name string) error {
if err != nil { if err != nil {
return err return err
} }
g.ctx.Textures[name] = NewTexture(bmp) g.ctx.Textures[name] = newTexture(bmp)
return nil return nil
} }
func (g *Game) loadFonts() error { func (g *game) loadFonts() error {
openSansPath, err := g.ctx.Resources.Retrieve("fonts/OpenSans-Regular.ttf") openSansPath, err := g.ctx.Resources.Retrieve("fonts/OpenSans-Regular.ttf")
if err != nil { if err != nil {
return err return err
@ -60,11 +67,7 @@ func (g *Game) loadFonts() error {
if err != nil { if err != nil {
return err return err
} }
err = g.ui.Fonts().LoadFonts(alui.FontDescription{Path: openSansPath, Name: "default", Size: 12}) err = g.ui.Fonts().LoadFonts(alui.FontDescription{Path: openSansPath, Name: "default", Size: 36})
if err != nil {
return err
}
err = g.ui.Fonts().LoadFonts(alui.FontDescription{Path: openSansPath, Name: "steps", Size: 36})
if err != nil { if err != nil {
return err return err
} }
@ -75,7 +78,7 @@ func (g *Game) loadFonts() error {
return nil return nil
} }
func (g *Game) loadTextures(pathToName map[string]string) error { func (g *game) loadTextures(pathToName map[string]string) error {
for path, name := range pathToName { for path, name := range pathToName {
err := g.loadBitmap(path, name) err := g.loadBitmap(path, name)
if err != nil { if err != nil {
@ -85,7 +88,7 @@ func (g *Game) loadTextures(pathToName map[string]string) error {
return nil return nil
} }
func (g *Game) loadLevels(names ...string) error { func (g *game) loadLevels(names ...string) error {
g.ctx.Levels = map[string]level{} g.ctx.Levels = map[string]level{}
for _, name := range names { for _, name := range names {
fileName := fmt.Sprintf("levels/level%s.txt", name) fileName := fmt.Sprintf("levels/level%s.txt", name)
@ -103,7 +106,7 @@ func (g *Game) loadLevels(names ...string) error {
return nil return nil
} }
func (g *Game) loadSprites(names ...string) error { func (g *game) loadSprites(names ...string) error {
g.ctx.Sprites = map[string]sprite{} g.ctx.Sprites = map[string]sprite{}
for _, name := range names { for _, name := range names {
fileName := fmt.Sprintf("sprites/%s.txt", name) fileName := fmt.Sprintf("sprites/%s.txt", name)
@ -130,7 +133,7 @@ func (g *Game) loadSprites(names ...string) error {
return nil return nil
} }
func (g *Game) loadAssets() error { 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",
@ -171,15 +174,14 @@ func (g *Game) loadAssets() error {
return nil return nil
} }
func (g *Game) Destroy() { func (g *game) Destroy() {
g.ui.Destroy() g.ui.Destroy()
g.ctx.Destroy() g.ctx.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{}, Settings: newDefaultSettings(), Navigation: navigation{game: g}}
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
} }
@ -188,11 +190,13 @@ func (g *Game) Init(disp *allg5.Display, res vfs.CopyDir, cons *gut.Console, fps
return err return err
} }
log.Print("Loaded assets.") log.Print("Loaded assets.")
g.playLevel("1")
g.ctx.Navigation.showMainMenu()
return nil return nil
} }
func (g *Game) Handle(e allg5.Event) { func (g *game) Handle(e allg5.Event) {
switch e := e.(type) { switch e := e.(type) {
case *allg5.KeyDownEvent: case *allg5.KeyDownEvent:
if e.KeyCode == allg5.KeyF3 { if e.KeyCode == allg5.KeyF3 {
@ -205,18 +209,4 @@ func (g *Game) Handle(e allg5.Event) {
g.ui.Handle(e) g.ui.Handle(e)
} }
func (g *Game) playLevel(l string) { func (g *game) Render() { g.ui.Render() }
play := &playLevel{ctx: g.ctx}
play.loadLevel(l)
g.scene.Proxy = play
}
func (g *Game) Render() {
switch scene := g.scene.Proxy.(type) {
case *splash:
if scene.atEnd {
g.playLevel("1")
}
}
g.ui.Render()
}

View File

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/krampus19/alui" "opslag.de/schobers/krampus19/alui"
"opslag.de/schobers/krampus19/gut" "opslag.de/schobers/krampus19/gut"
@ -16,5 +15,5 @@ type info struct {
} }
func (i *info) Render(ctx *alui.Context, bounds geom.RectangleF32) { func (i *info) Render(ctx *alui.Context, bounds geom.RectangleF32) {
ctx.Fonts.Get("default").Draw(4, 4, allg5.NewColor(0xff, 0xff, 0xff), allg5.AlignLeft, fmt.Sprintf("FPS: %d", i.fps.Current())) ctx.Fonts.Draw("console", 4, 4, ctx.Palette.Text, fmt.Sprintf("FPS: %d", i.fps.Current()))
} }

View File

@ -29,16 +29,17 @@ func resources() (vfs.CopyDir, error) {
} }
func run() error { func run() error {
defer log.Printf("All cleaned up, bye bye!")
cons := &gut.Console{} cons := &gut.Console{}
log.SetOutput(io.MultiWriter(log.Writer(), cons)) log.SetOutput(io.MultiWriter(log.Writer(), cons))
log.Println("Initializing Allegro") log.Printf("Initializing Allegro")
err := allg5.Init(allg5.InitAll) err := allg5.Init(allg5.InitAll)
if err != nil { if err != nil {
return err return err
} }
log.Println("Creating display") log.Printf("Creating display")
disp, err := allg5.NewDisplay(1440, 900, allg5.NewDisplayOptions{Maximized: false, Windowed: true, Resizable: true, Vsync: true}) disp, err := allg5.NewDisplay(1440, 900, allg5.NewDisplayOptions{Maximized: false, Windowed: true, Resizable: true, Vsync: true})
if err != nil { if err != nil {
return err return err
@ -64,20 +65,20 @@ func run() error {
fps := gut.NewFPS() fps := gut.NewFPS()
defer fps.Destroy() defer fps.Destroy()
game := &Game{} game := &game{}
err = game.Init(disp, res, cons, fps) err = game.Init(disp, res, cons, fps)
if err != nil { if err != nil {
return err return err
} }
defer game.Destroy() defer game.Destroy()
log.Println("Starting game loop") log.Printf("Starting game loop")
back := allg5.NewColor(0x21, 0x21, 0x21) defer log.Printf("Stopped game loop, cleaning up")
start := time.Now() start := time.Now()
for { for {
game.ctx.Tick = time.Now().Sub(start) game.ctx.Tick = time.Now().Sub(start)
allg5.ClearToColor(back) allg5.ClearToColor(game.ctx.Palette.Icon)
game.Render() game.Render()
disp.Flip() disp.Flip()
fps.Count() fps.Count()
@ -86,14 +87,21 @@ func run() error {
for e != nil { for e != nil {
switch e := e.(type) { switch e := e.(type) {
case *allg5.DisplayCloseEvent: case *allg5.DisplayCloseEvent:
log.Printf("Stopping game loop, user closed display")
return nil return nil
case *allg5.KeyDownEvent: case *allg5.KeyCharEvent:
if e.KeyCode == allg5.KeyEscape { if e.KeyCode == allg5.KeyF4 && e.Modifiers&allg5.KeyModAlt != 0 {
log.Printf("Stopping game loop, user pressed Alt+F4")
return nil return nil
} }
} }
game.Handle(e) game.Handle(e)
e = eq.Get() e = eq.Get()
} }
if game.scene.Proxy == nil {
log.Printf("Stopping game loop, user quit via an in-game option")
return nil
}
} }
} }

View File

@ -1,3 +1,41 @@
package main package main
type mainMenu struct{} import (
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
"opslag.de/schobers/krampus19/alui"
)
type mainMenu struct {
alui.ControlBase
ctx *Context
buttons alui.StackPanel
}
func (m *mainMenu) newButton(text string, onClick func()) alui.Control {
button := &alui.Button{Text: text, TextAlign: allg5.AlignCenter}
button.OnClick = onClick
return button
}
func (m *mainMenu) Enter(ctx *Context) error {
m.ctx = ctx
m.buttons.Orientation = alui.OrientationVertical
m.buttons.Children = append(m.buttons.Children, m.newButton("Play", func() { m.ctx.Navigation.playLevel("1") }))
m.buttons.Children = append(m.buttons.Children, m.newButton("Quit", func() { m.ctx.Navigation.quit() }))
return nil
}
func (m *mainMenu) Leave() {}
func (m *mainMenu) Handle(e allg5.Event) { m.buttons.Handle(e) }
func (m *mainMenu) Render(ctx *alui.Context, bounds geom.RectangleF32) {
buttonsHeight := m.buttons.DesiredSize(ctx).Y
width, center := bounds.Dx(), bounds.Center()
buttonsBounds := geom.RectF32(.25*width, center.Y-.5*buttonsHeight, .75*width, center.Y+.5*buttonsHeight)
m.buttons.Layout(ctx, buttonsBounds)
m.buttons.Render(ctx, buttonsBounds)
}

View File

@ -0,0 +1,40 @@
package main
import (
"opslag.de/schobers/krampus19/alui"
)
type navigation struct {
game *game
curr scene
}
type scene interface {
alui.Control
Enter(ctx *Context) error
Leave()
}
func (n *navigation) playLevel(l string) {
n.switchTo(&playLevel{name: l})
}
func (n *navigation) quit() {
n.switchTo(nil)
}
func (n *navigation) showMainMenu() {
n.switchTo(&mainMenu{})
}
func (n *navigation) switchTo(s scene) {
if n.curr != nil {
n.curr.Leave()
}
n.curr = s
n.game.scene.Proxy = s
if n.curr != nil {
n.curr.Enter(n.game.ctx)
}
}

View File

@ -15,6 +15,7 @@ import (
type playLevel struct { type playLevel struct {
alui.ControlBase alui.ControlBase
name string
ctx *Context ctx *Context
offset geom.PointF32 offset geom.PointF32
scale float32 scale float32
@ -80,9 +81,9 @@ func (a *entityMoveAnimation) Animate(start, now time.Duration) bool {
return true return true
} }
func (s *playLevel) idxToPos(i int) geom.PointF32 { return s.level.idxToPos(i).ToF32() } func (l *playLevel) idxToPos(i int) geom.PointF32 { return l.level.idxToPos(i).ToF32() }
func (s *playLevel) isIdle() bool { return s.ani.Idle() } func (l *playLevel) isIdle() bool { return l.ani.Idle() }
func findEntityAt(entities []*entity, pos geom.Point) *entity { func findEntityAt(entities []*entity, pos geom.Point) *entity {
idx := findEntityIdx(entities, pos) idx := findEntityIdx(entities, pos)
@ -101,48 +102,48 @@ func findEntityIdx(entities []*entity, pos geom.Point) int {
return -1 return -1
} }
func (s *playLevel) findEntityAt(pos geom.Point) *entity { func (l *playLevel) findEntityAt(pos geom.Point) *entity {
if s.player.pos == pos { if l.player.pos == pos {
return s.player return l.player
} }
brick := findEntityAt(s.bricks, pos) brick := findEntityAt(l.bricks, pos)
if brick != nil { if brick != nil {
return brick return brick
} }
return findEntityAt(s.sunken, pos) return findEntityAt(l.sunken, pos)
} }
func (s *playLevel) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool) bool { func (l *playLevel) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool) bool {
return s.checkTileNotFound(pos, check, false) return l.checkTileNotFound(pos, check, false)
} }
func (s *playLevel) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool, notFound bool) bool { func (l *playLevel) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool, notFound bool) bool {
idx := s.level.posToIdx(pos) idx := l.level.posToIdx(pos)
if idx == -1 { if idx == -1 {
return notFound return notFound
} }
return check(pos, idx, s.level.tiles[idx]) return check(pos, idx, l.level.tiles[idx])
} }
func (s *playLevel) isSolidTile(pos geom.Point, idx int, t tile) bool { func (l *playLevel) isSolidTile(pos geom.Point, idx int, t tile) bool {
switch t { switch t {
case tileBasic: case tileBasic:
return true return true
case tileWater: case tileWater:
return findEntityAt(s.sunken, pos) != nil return findEntityAt(l.sunken, pos) != nil
} }
return false return false
} }
func (s *playLevel) wouldBrickSink(pos geom.Point, idx int, t tile) bool { func (l *playLevel) wouldBrickSink(pos geom.Point, idx int, t tile) bool {
return t == tileWater && findEntityAt(s.sunken, pos) == nil return t == tileWater && findEntityAt(l.sunken, pos) == nil
} }
func (s *playLevel) isObstructed(pos geom.Point, idx int, t tile) bool { func (l *playLevel) isObstructed(pos geom.Point, idx int, t tile) bool {
if findEntityAt(s.bricks, pos) != nil { if findEntityAt(l.bricks, pos) != nil {
return true // brick return true // brick
} }
switch s.level.tiles[idx] { switch l.level.tiles[idx] {
case tileWater: case tileWater:
return false return false
case tileBasic: case tileBasic:
@ -151,45 +152,50 @@ func (s *playLevel) isObstructed(pos geom.Point, idx int, t tile) bool {
return true return true
} }
func (s *playLevel) canMove(from, dir geom.Point) bool { func (l *playLevel) canMove(from, dir geom.Point) bool {
to := from.Add(dir) to := from.Add(dir)
if !s.checkTile(to, s.isSolidTile) { if !l.checkTile(to, l.isSolidTile) {
return false return false
} }
brick := findEntityAt(s.bricks, to) brick := findEntityAt(l.bricks, to)
if brick != nil { if brick != nil {
brickTo := to.Add(dir) brickTo := to.Add(dir)
return !s.checkTileNotFound(brickTo, s.isObstructed, true) return !l.checkTileNotFound(brickTo, l.isObstructed, true)
} }
return true return true
} }
func (s *playLevel) loadLevel(name string) { func (l *playLevel) Enter(ctx *Context) error {
s.keysDown = keyPressedState{} l.ctx = ctx
s.level = s.ctx.Levels[name]
s.bricks = nil l.keysDown = keyPressedState{}
s.sunken = nil l.level = l.ctx.Levels[l.name]
for i, e := range s.level.entities { l.bricks = nil
l.sunken = nil
for i, e := range l.level.entities {
switch e { switch e {
case entityTypeBrick: case entityTypeBrick:
s.bricks = append(s.bricks, newEntity(e, s.level.idxToPos(i))) l.bricks = append(l.bricks, newEntity(e, l.level.idxToPos(i)))
case entityTypeCharacter: case entityTypeCharacter:
s.player = newEntity(e, s.level.idxToPos(i)) l.player = newEntity(e, l.level.idxToPos(i))
case entityTypeVillain: case entityTypeVillain:
s.villain = newEntity(e, s.level.idxToPos(i)) l.villain = newEntity(e, l.level.idxToPos(i))
} }
} }
return nil
} }
func (s *playLevel) posToScreen(p geom.Point) geom.PointF32 { func (l *playLevel) Leave() {}
return s.posToScreenF32(p.ToF32())
func (l *playLevel) posToScreen(p geom.Point) geom.PointF32 {
return l.posToScreenF32(p.ToF32())
} }
func (s *playLevel) posToScreenF32(p geom.PointF32) geom.PointF32 { func (l *playLevel) posToScreenF32(p geom.PointF32) geom.PointF32 {
pos := p.Add2D(.5, .5) pos := p.Add2D(.5, .5)
pos = geom.PtF32(pos.X*148-pos.Y*46, pos.Y*82) pos = geom.PtF32(pos.X*148-pos.Y*46, pos.Y*82)
pos = geom.PtF32(pos.X*s.scale, pos.Y*s.scale) pos = geom.PtF32(pos.X*l.scale, pos.Y*l.scale)
return pos.Add(s.offset) return pos.Add(l.offset)
} }
// <- 168-> // <- 168->
@ -205,90 +211,90 @@ func (s *playLevel) posToScreenF32(p geom.PointF32) geom.PointF32 {
// Offset between horizontal tiles: 148,0 // Offset between horizontal tiles: 148,0
// Offset between vertical tiles: -46,82 // Offset between vertical tiles: -46,82
func (s *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) { func (l *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) {
s.scale = bounds.Dy() / 1080 l.scale = bounds.Dy() / 1080
tilesCenter := geom.PtF32(.5*float32(s.level.width), .5*float32(s.level.height)) tilesCenter := geom.PtF32(.5*float32(l.level.width), .5*float32(l.level.height))
tilesCenter = geom.PtF32(tilesCenter.X*148-tilesCenter.Y*46, tilesCenter.Y*82) tilesCenter = geom.PtF32(tilesCenter.X*148-tilesCenter.Y*46, tilesCenter.Y*82)
tilesCenter = geom.PtF32(tilesCenter.X*s.scale, tilesCenter.Y*s.scale) tilesCenter = geom.PtF32(tilesCenter.X*l.scale, tilesCenter.Y*l.scale)
center := bounds.Center() center := bounds.Center()
s.offset = geom.PtF32(center.X-tilesCenter.X, center.Y-tilesCenter.Y) l.offset = geom.PtF32(center.X-tilesCenter.X, center.Y-tilesCenter.Y)
s.ani.Animate(s.ctx.Tick) l.ani.Animate(l.ctx.Tick)
} }
func (s *playLevel) tryPlayerMove(dir geom.Point, key allg5.Key) { func (l *playLevel) tryPlayerMove(dir geom.Point, key allg5.Key) {
if !s.isIdle() { if !l.isIdle() {
return return
} }
to := s.player.pos.Add(dir) to := l.player.pos.Add(dir)
if !s.canMove(s.player.pos, dir) { if !l.canMove(l.player.pos, dir) {
log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key)) log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key))
return return
} }
s.steps++ l.steps++
log.Printf("Moving player to %s", to) log.Printf("Moving player to %s", to)
s.ani.StartFn(s.ctx.Tick, newEntityMoveAnimation(s.player, to), func() { l.ani.StartFn(l.ctx.Tick, newEntityMoveAnimation(l.player, to), func() {
log.Printf("Player movement finished") log.Printf("Player movement finished")
if s.keysDown[key] && s.keysDown.CountPressed(s.ctx.Settings.Controls.MovementKeys()...) == 1 { if l.keysDown[key] && l.keysDown.CountPressed(l.ctx.Settings.Controls.MovementKeys()...) == 1 {
log.Printf("Key %s is still down, moving further", gut.KeyToString(key)) log.Printf("Key %s is still down, moving further", gut.KeyToString(key))
s.tryPlayerMove(dir, key) l.tryPlayerMove(dir, key)
} }
}) })
if brick := findEntityAt(s.bricks, to); brick != nil { if brick := findEntityAt(l.bricks, to); brick != nil {
log.Printf("Pushing brick at %s", to) log.Printf("Pushing brick at %s", to)
brickTo := to.Add(dir) brickTo := to.Add(dir)
s.ani.StartFn(s.ctx.Tick, newEntityMoveAnimation(brick, brickTo), func() { l.ani.StartFn(l.ctx.Tick, newEntityMoveAnimation(brick, brickTo), func() {
log.Printf("Brick movement finished") log.Printf("Brick movement finished")
if s.checkTile(brickTo, s.wouldBrickSink) { if l.checkTile(brickTo, l.wouldBrickSink) {
log.Printf("Sinking brick at %s", brickTo) log.Printf("Sinking brick at %s", brickTo)
idx := findEntityIdx(s.bricks, brickTo) idx := findEntityIdx(l.bricks, brickTo)
s.bricks = append(s.bricks[:idx], s.bricks[idx+1:]...) l.bricks = append(l.bricks[:idx], l.bricks[idx+1:]...)
s.sunken = append(s.sunken, brick) l.sunken = append(l.sunken, brick)
} }
}) })
} }
} }
func (s *playLevel) Handle(e allg5.Event) { func (l *playLevel) Handle(e allg5.Event) {
switch e := e.(type) { switch e := e.(type) {
case *allg5.KeyDownEvent: case *allg5.KeyDownEvent:
s.keysDown[e.KeyCode] = true l.keysDown[e.KeyCode] = true
switch e.KeyCode { switch e.KeyCode {
case s.ctx.Settings.Controls.MoveUp: case l.ctx.Settings.Controls.MoveUp:
s.tryPlayerMove(geom.Pt(0, -1), e.KeyCode) l.tryPlayerMove(geom.Pt(0, -1), e.KeyCode)
case s.ctx.Settings.Controls.MoveRight: case l.ctx.Settings.Controls.MoveRight:
s.tryPlayerMove(geom.Pt(1, 0), e.KeyCode) l.tryPlayerMove(geom.Pt(1, 0), e.KeyCode)
case s.ctx.Settings.Controls.MoveDown: case l.ctx.Settings.Controls.MoveDown:
s.tryPlayerMove(geom.Pt(0, 1), e.KeyCode) l.tryPlayerMove(geom.Pt(0, 1), e.KeyCode)
case s.ctx.Settings.Controls.MoveLeft: case l.ctx.Settings.Controls.MoveLeft:
s.tryPlayerMove(geom.Pt(-1, 0), e.KeyCode) l.tryPlayerMove(geom.Pt(-1, 0), e.KeyCode)
} }
case *allg5.KeyUpEvent: case *allg5.KeyUpEvent:
s.keysDown[e.KeyCode] = false l.keysDown[e.KeyCode] = false
} }
} }
func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
basicTile := s.ctx.Textures["basic_tile"] basicTile := l.ctx.Textures["basic_tile"]
waterTile := s.ctx.Textures["water_tile"] waterTile := l.ctx.Textures["water_tile"]
sunkenBrickTile := s.ctx.Textures["sunken_brick_tile"] sunkenBrickTile := l.ctx.Textures["sunken_brick_tile"]
opts := allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * (168 / float32(basicTile.Width())))} opts := allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(l.scale * (168 / float32(basicTile.Width())))}
level := s.level level := l.level
for i, t := range level.tiles { for i, t := range level.tiles {
pos := geom.Pt(i%level.width, i/level.width) pos := geom.Pt(i%level.width, i/level.width)
scrPos := s.posToScreen(pos) scrPos := l.posToScreen(pos)
switch t { switch t {
case tileBasic: case tileBasic:
basicTile.DrawOptions(scrPos.X, scrPos.Y, opts) basicTile.DrawOptions(scrPos.X, scrPos.Y, opts)
case tileWater: case tileWater:
scrPos := s.posToScreenF32(pos.ToF32().Add2D(0, .2*s.scale)) scrPos := l.posToScreenF32(pos.ToF32().Add2D(0, .2*l.scale))
if findEntityAt(s.sunken, pos) == nil { if findEntityAt(l.sunken, pos) == nil {
waterTile.DrawOptions(scrPos.X, scrPos.Y, opts) waterTile.DrawOptions(scrPos.X, scrPos.Y, opts)
} else { } else {
sunkenBrickTile.DrawOptions(scrPos.X, scrPos.Y, opts) sunkenBrickTile.DrawOptions(scrPos.X, scrPos.Y, opts)
@ -296,15 +302,15 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
} }
} }
character := s.ctx.Textures["main_character"] character := l.ctx.Textures["main_character"]
villain := s.ctx.Textures["villain_character"] villain := l.ctx.Textures["villain_character"]
brick := s.ctx.Textures["brick"] brick := l.ctx.Textures["brick"]
crate := s.ctx.Textures["crate"] crate := l.ctx.Textures["crate"]
var entities []*entity var entities []*entity
entities = append(entities, s.player) entities = append(entities, l.player)
entities = append(entities, s.villain) entities = append(entities, l.villain)
entities = append(entities, s.bricks...) entities = append(entities, l.bricks...)
sort.Slice(entities, func(i, j int) bool { sort.Slice(entities, func(i, j int) bool {
if entities[i].scr.Y == entities[j].scr.Y { if entities[i].scr.Y == entities[j].scr.Y {
@ -314,27 +320,26 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
}) })
for _, e := range entities { for _, e := range entities {
scrPos := s.posToScreenF32(e.scr) scrPos := l.posToScreenF32(e.scr)
switch e.typ { switch e.typ {
case entityTypeCharacter: case entityTypeCharacter:
scrPos := s.posToScreenF32(e.scr.Add2D(-.2*s.scale, -.9*s.scale)) scrPos := l.posToScreenF32(e.scr.Add2D(-.2*l.scale, -.9*l.scale))
character.DrawOptions(scrPos.X, scrPos.Y, opts) character.DrawOptions(scrPos.X, scrPos.Y, opts)
case entityTypeVillain: case entityTypeVillain:
scrPos := s.posToScreenF32(e.scr.Add2D(-.2*s.scale, -.9*s.scale)) scrPos := l.posToScreenF32(e.scr.Add2D(-.2*l.scale, -.9*l.scale))
villain.DrawOptions(scrPos.X, scrPos.Y, opts) villain.DrawOptions(scrPos.X, scrPos.Y, opts)
case entityTypeBrick: case entityTypeBrick:
if findEntityAt(s.bricks, e.pos) == nil { if findEntityAt(l.bricks, e.pos) == nil {
break break
} }
scrPos := s.posToScreenF32(e.scr.Add2D(-.2*s.scale, -.7*s.scale)) scrPos := l.posToScreenF32(e.scr.Add2D(-.2*l.scale, -.7*l.scale))
brick.DrawOptions(scrPos.X, scrPos.Y, opts) brick.DrawOptions(scrPos.X, scrPos.Y, opts)
case entityTypeCrate: case entityTypeCrate:
crate.DrawOptions(scrPos.X, scrPos.Y, opts) crate.DrawOptions(scrPos.X, scrPos.Y, opts)
} }
} }
steps := fmt.Sprintf("STEPS: %d", s.steps) font := ctx.Fonts.Get("default")
stepsFont := ctx.Fonts.Get("steps") steps := fmt.Sprintf("STEPS: %d", l.steps)
stepsWidth := stepsFont.TextWidth(steps) ctx.Fonts.DrawAlignFont(font, bounds.Min.X, 24, bounds.Max.X, ctx.Palette.Text, allg5.AlignCenter, steps)
stepsFont.Draw(.5*(bounds.Dx()-stepsWidth), 24, allg5.NewColor(255, 255, 255), allg5.AlignCenter, steps)
} }

View File

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