From 11ab3fca0fe7edd6317fde8b8034ab3ad1de7188 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Sat, 28 Dec 2019 16:03:57 +0100 Subject: [PATCH] Added settings in-game. Added video settings. Added and improved reusable controls. Separated drawing of sprites. Fixed bug that text was right aligned instead of left aligned. --- alui/button.go | 6 + alui/center.go | 23 +++ alui/column.go | 34 ++++ alui/container.go | 14 +- alui/fonts.go | 2 +- alui/label.go | 5 +- alui/menu.go | 23 +-- alui/stackpanel.go | 8 +- cmd/krampus19/changesettings.go | 317 +++++++++++++++++++++++++++++++ cmd/krampus19/context.go | 15 +- cmd/krampus19/game.go | 11 +- cmd/krampus19/krampus19.go | 37 +++- cmd/krampus19/mainmenu.go | 1 + cmd/krampus19/navigation.go | 5 + cmd/krampus19/playlevel.go | 42 ++-- cmd/krampus19/res/sprites/ui.txt | 32 ++++ cmd/krampus19/res/ui.png | Bin 0 -> 34252 bytes cmd/krampus19/settings.go | 77 +++++++- cmd/krampus19/spritedrawer.go | 69 +++++++ 19 files changed, 641 insertions(+), 80 deletions(-) create mode 100644 alui/center.go create mode 100644 alui/column.go create mode 100644 cmd/krampus19/changesettings.go create mode 100644 cmd/krampus19/res/sprites/ui.txt create mode 100644 cmd/krampus19/res/ui.png create mode 100644 cmd/krampus19/spritedrawer.go diff --git a/alui/button.go b/alui/button.go index e0d6336..fcdd2ba 100644 --- a/alui/button.go +++ b/alui/button.go @@ -14,6 +14,12 @@ type Button struct { TextAlign allg5.HorizontalAlignment } +func NewButton(text string, onClick func()) *Button { + b := &Button{Text: text} + b.OnClick = onClick + return b +} + func (b *Button) DesiredSize(ctx *Context) geom.PointF32 { font := ctx.Fonts.Get(b.Font) w := font.TextWidth(b.Text) diff --git a/alui/center.go b/alui/center.go new file mode 100644 index 0000000..33963a0 --- /dev/null +++ b/alui/center.go @@ -0,0 +1,23 @@ +package alui + +import ( + "opslag.de/schobers/geom" +) + +type center struct { + Proxy +} + +func Center(control Control) Control { + return ¢er{Proxy: Proxy{Target: control}} +} + +func (c *center) Layout(ctx *Context, bounds geom.RectangleF32) { + size := c.DesiredSize(ctx) + center := bounds.Center() + + size.X = geom.Min32(size.X, bounds.Dx()) + size.Y = geom.Min32(size.Y, bounds.Dy()) + size = size.Mul(.5) + c.Proxy.Layout(ctx, geom.RectF32(center.X-size.X, center.Y-size.Y, center.X+size.X, center.Y+size.Y)) +} diff --git a/alui/column.go b/alui/column.go new file mode 100644 index 0000000..bb04f11 --- /dev/null +++ b/alui/column.go @@ -0,0 +1,34 @@ +package alui + +import "opslag.de/schobers/geom" + +type Column struct { + Proxy + + panel *StackPanel +} + +func NewColumn() *Column { + c := &Column{} + c.Init() + return c +} + +func (c *Column) AddChild(child ...Control) { + c.panel.Children = append(c.panel.Children, child...) +} + +func (c *Column) Init() { + c.panel = &StackPanel{Orientation: OrientationVertical} + c.Proxy.Target = c.panel +} + +func (c *Column) Layout(ctx *Context, bounds geom.RectangleF32) {} + +func (c *Column) Render(ctx *Context, bounds geom.RectangleF32) { + columnHeight := c.Proxy.DesiredSize(ctx).Y + width, center := bounds.Dx(), bounds.Center() + columnBounds := geom.RectF32(.25*width, center.Y-.5*columnHeight, .75*width, center.Y+.5*columnHeight) + c.Proxy.Layout(ctx, columnBounds) + c.Proxy.Render(ctx, columnBounds) +} diff --git a/alui/container.go b/alui/container.go index 2e545fa..f672393 100644 --- a/alui/container.go +++ b/alui/container.go @@ -11,11 +11,8 @@ type Container struct { Children []Control } -func (c *Container) Handle(e allg5.Event) { - c.ControlBase.Handle(e) - for _, child := range c.Children { - child.Handle(e) - } +func (c *Container) AddChild(child ...Control) { + c.Children = append(c.Children, child...) } func (c *Container) DesiredSize(ctx *Context) geom.PointF32 { @@ -36,6 +33,13 @@ func (c *Container) DesiredSize(ctx *Context) geom.PointF32 { return size } +func (c *Container) Handle(e allg5.Event) { + c.ControlBase.Handle(e) + for _, child := range c.Children { + child.Handle(e) + } +} + func (c *Container) Layout(ctx *Context, bounds geom.RectangleF32) { c.ControlBase.Layout(ctx, bounds) for _, child := range c.Children { diff --git a/alui/fonts.go b/alui/fonts.go index 3ae288d..c80a4bb 100644 --- a/alui/fonts.go +++ b/alui/fonts.go @@ -44,7 +44,7 @@ func (f *Fonts) DrawAlignFont(font *allg5.Font, left, top, right float32, color 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) + font.Draw(left, top, color, allg5.AlignLeft, text) } } diff --git a/alui/label.go b/alui/label.go index 617a9b8..2d62779 100644 --- a/alui/label.go +++ b/alui/label.go @@ -10,7 +10,8 @@ var _ Control = &Label{} type Label struct { ControlBase - Text string + Text string + TextAlign allg5.HorizontalAlignment } func (l *Label) DesiredSize(ctx *Context) geom.PointF32 { @@ -29,5 +30,5 @@ func (l *Label) Render(ctx *Context, bounds geom.RectangleF32) { if back != nil { allg5.DrawFilledRectangle(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, *back) } - ctx.Fonts.Draw(l.Font, bounds.Min.X+4, bounds.Min.Y+4, *fore, l.Text) + ctx.Fonts.DrawAlign(l.Font, bounds.Min.X+4, bounds.Min.Y+4, bounds.Max.X-4, *fore, l.TextAlign, l.Text) } diff --git a/alui/menu.go b/alui/menu.go index 21ba853..3aa532c 100644 --- a/alui/menu.go +++ b/alui/menu.go @@ -2,13 +2,11 @@ package alui import ( "opslag.de/schobers/allg5" - "opslag.de/schobers/geom" ) type Menu struct { - Proxy + Column - panel *StackPanel active int buttons []*Button } @@ -19,11 +17,6 @@ func NewMenu() *Menu { return m } -func (m *Menu) Init() { - m.panel = &StackPanel{Orientation: OrientationVertical} - m.Proxy.Target = m.panel -} - func (m *Menu) Activate(i int) { if len(m.buttons) == 0 || i < 0 { return @@ -42,11 +35,11 @@ func (m *Menu) Add(text string, onClick func()) { button.Over = true } m.buttons = append(m.buttons, button) - m.panel.Children = append(m.panel.Children, button) + m.AddChild(button) } func (m *Menu) Handle(e allg5.Event) { - m.Proxy.Handle(e) + m.Column.Handle(e) if len(m.buttons) == 0 { return @@ -75,16 +68,6 @@ func (m *Menu) Handle(e allg5.Event) { } } -func (m *Menu) Layout(ctx *Context, bounds geom.RectangleF32) {} - -func (m *Menu) Render(ctx *Context, bounds geom.RectangleF32) { - menuHeight := m.Proxy.DesiredSize(ctx).Y - width, center := bounds.Dx(), bounds.Center() - menuBounds := geom.RectF32(.25*width, center.Y-.5*menuHeight, .75*width, center.Y+.5*menuHeight) - m.Proxy.Layout(ctx, menuBounds) - m.Proxy.Render(ctx, menuBounds) -} - func (m *Menu) updateActiveButton(active int) { m.active = active for i, btn := range m.buttons { diff --git a/alui/stackpanel.go b/alui/stackpanel.go index dc5e0bc..57fee2f 100644 --- a/alui/stackpanel.go +++ b/alui/stackpanel.go @@ -13,10 +13,6 @@ type StackPanel struct { Orientation Orientation } -func (s *StackPanel) Handle(e allg5.Event) { - s.Container.Handle(e) -} - func (s *StackPanel) asLength(p geom.PointF32) float32 { switch s.Orientation { case OrientationHorizontal: @@ -75,6 +71,10 @@ func (s *StackPanel) DesiredSize(ctx *Context) geom.PointF32 { return size } +func (s *StackPanel) Handle(e allg5.Event) { + s.Container.Handle(e) +} + func (s *StackPanel) Layout(ctx *Context, bounds geom.RectangleF32) { s.Container.Layout(ctx, bounds) diff --git a/cmd/krampus19/changesettings.go b/cmd/krampus19/changesettings.go new file mode 100644 index 0000000..d1cb8f7 --- /dev/null +++ b/cmd/krampus19/changesettings.go @@ -0,0 +1,317 @@ +package main + +import ( + "fmt" + "log" + + "opslag.de/schobers/krampus19/gut" + + "opslag.de/schobers/geom" + + "opslag.de/schobers/allg5" + "opslag.de/schobers/krampus19/alui" +) + +const margin = 8 + +type displayModeControl struct { + alui.StackPanel + + label alui.Label + + modes []string + current int +} + +func newDisplayModeControl(ctx *Context) *displayModeControl { + var modes []string + fmtDisplayMode := func(width, height int) string { return fmt.Sprintf("%d x %d", width, height) } + containsDisplayMode := func(mode string) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false + } + + displayMode := ctx.Settings.Video.DisplayMode + if displayMode == "" { + displayMode = fmtDisplayMode(ctx.DisplaySize.X, ctx.DisplaySize.Y) + } + var current int + for _, m := range allg5.DisplayModes() { + mode := fmtDisplayMode(m.Width, m.Height) + if containsDisplayMode(mode) { + continue + } + if mode == displayMode { + current = len(modes) + } + modes = append(modes, mode) + } + + c := &displayModeControl{modes: modes, current: current} + c.Orientation = alui.OrientationHorizontal + c.label.TextAlign = allg5.AlignCenter + c.selectMode(0) + + c.AddChild(newSpriteButton(ctx, "ui", "angle-left", func() { c.selectMode(-1) })) + c.AddChild(&c.label) + c.AddChild(newSpriteButton(ctx, "ui", "angle-right", func() { c.selectMode(1) })) + return c +} + +func (c *displayModeControl) selectMode(delta int) { + c.current = (c.current + delta + len(c.modes)) % len(c.modes) + c.label.Text = c.Mode() +} + +func (c *displayModeControl) Mode() string { return c.modes[c.current] } + +type checkBox struct { + alui.Button + + ctx *Context + + Selected bool +} + +func newCheckBox(ctx *Context) *checkBox { + b := &checkBox{ctx: ctx} + b.OnClick = func() { + b.Selected = !b.Selected + } + return b +} + +func (b *checkBox) DesiredSize(ctx *alui.Context) geom.PointF32 { + return b.ctx.SpriteDrawer.Size("ui", "check-square").Add2D(2*margin, 2*margin) +} + +func (b *checkBox) Render(ctx *alui.Context, bounds geom.RectangleF32) { + tint := b.ctx.Palette.Primary + if b.Over { + tint = b.ctx.Palette.Dark + ctx.Cursor = allg5.MouseCursorLink + } + var part = "square" + if b.Selected { + part = "check-square" + } + b.ctx.SpriteDrawer.Draw("ui", part, bounds.Center(), DrawSpriteOptions{Tint: &tint}) +} + +type spriteButton struct { + alui.Button + + ctx *Context + + Sprite string + Part string +} + +func newSpriteButton(ctx *Context, sprite, part string, onClick func()) *spriteButton { + b := &spriteButton{ctx: ctx, Sprite: sprite, Part: part} + b.OnClick = onClick + return b +} + +func (b *spriteButton) DesiredSize(ctx *alui.Context) geom.PointF32 { + return b.ctx.SpriteDrawer.Size(b.Sprite, b.Part).Add2D(2*margin, 2*margin) +} + +func (b *spriteButton) Render(ctx *alui.Context, bounds geom.RectangleF32) { + tint := b.ctx.Palette.Primary + if b.Over { + tint = b.ctx.Palette.Dark + ctx.Cursor = allg5.MouseCursorLink + } + b.ctx.SpriteDrawer.Draw(b.Sprite, b.Part, bounds.Center(), DrawSpriteOptions{Tint: &tint}) +} + +type selectKeyControl struct { + alui.Label + + Key allg5.Key + + WaitingForInput bool +} + +func newSelectKeyControl(key allg5.Key) *selectKeyControl { + c := &selectKeyControl{Key: key} + c.TextAlign = allg5.AlignCenter + return c +} + +func (c *selectKeyControl) Handle(e allg5.Event) { + c.Label.Handle(e) + + switch e := e.(type) { + case *allg5.MouseButtonDownEvent: + if c.Over && e.Button == allg5.MouseButtonLeft { + c.WaitingForInput = true + } + case *allg5.KeyDownEvent: + if c.WaitingForInput { + if e.KeyCode != allg5.KeyEscape { + c.Key = e.KeyCode + } + c.WaitingForInput = false + } + } +} + +func (c *selectKeyControl) Render(ctx *alui.Context, bounds geom.RectangleF32) { + if c.WaitingForInput { + c.Text = "_" + } else { + c.Text = gut.KeyToString(c.Key) + } + if c.Over { + ctx.Cursor = allg5.MouseCursorLink + } + c.Label.Render(ctx, bounds) +} + +type settingsHeader struct { + alui.Label +} + +func newSettingsHeader(label string) alui.Control { + header := &settingsHeader{} + header.Text = label + return header +} + +func (h *settingsHeader) DesiredSize(ctx *alui.Context) geom.PointF32 { + size := h.Label.DesiredSize(ctx) + size.Y += 5 * margin + return size +} + +func (h *settingsHeader) Layout(ctx *alui.Context, bounds geom.RectangleF32) { + var label = bounds + if label.Dy() > 5*margin { + label.Min.Y = bounds.Min.Y + 3*margin + label.Max.Y = bounds.Max.Y - 2*margin + } + h.Label.Layout(ctx, label) +} + +type settingsRow struct { + alui.StackPanel + + label *alui.Label + edit alui.Control +} + +func newSettingRow(label string, edit alui.Control) *settingsRow { + row := &settingsRow{} + row.Orientation = alui.OrientationHorizontal + row.label = &alui.Label{Text: label} + row.edit = edit + row.Children = append(row.Children, row.label, row.edit) + return row +} + +func (r *settingsRow) Layout(ctx *alui.Context, bounds geom.RectangleF32) { + label := bounds + label.Max.X = label.Min.X + .5*label.Dx() + r.label.Layout(ctx, label) + + width := r.edit.DesiredSize(ctx).X + edit := bounds + if geom.IsNaN32(width) || width > label.Dx() { + edit.Min.X = label.Max.X + } else { + margin := (label.Dx() - width) * .5 + edit.Min.X = label.Max.X + margin + edit.Max.X -= margin + } + r.edit.Layout(ctx, edit) +} + +type wideButton struct { + alui.Button +} + +func newWideButton(label string, onClick func()) *wideButton { + b := &wideButton{} + b.Text = label + b.TextAlign = allg5.AlignCenter + b.OnClick = onClick + return b +} + +func (b *wideButton) DesiredSize(ctx *alui.Context) geom.PointF32 { + size := b.Button.DesiredSize(ctx) + size.X += 2 * margin + return size +} + +type changeSettings struct { + alui.Column + + ctx *Context + + rows []*settingsRow + active int +} + +func (s *changeSettings) addRow(row ...*settingsRow) { + for _, row := range row { + s.AddChild(row) + s.rows = append(s.rows, row) + } +} + +func (s *changeSettings) Enter(ctx *Context) error { + s.Init() + s.ctx = ctx + + keyLeft := newSelectKeyControl(s.ctx.Settings.Controls.MoveLeft) + keyUp := newSelectKeyControl(s.ctx.Settings.Controls.MoveUp) + keyRight := newSelectKeyControl(s.ctx.Settings.Controls.MoveRight) + keyDown := newSelectKeyControl(s.ctx.Settings.Controls.MoveDown) + + s.AddChild(newSettingsHeader("Controls")) + s.addRow( + newSettingRow("Key left", keyLeft), + newSettingRow("Key up", keyUp), + newSettingRow("Key right", keyRight), + newSettingRow("Key down", keyDown), + ) + s.AddChild(newSettingsHeader("Video")) + + displayMode := newDisplayModeControl(ctx) + windowed := newCheckBox(ctx) + windowed.Selected = s.ctx.Settings.Video.Windowed + + s.addRow( + newSettingRow("Windowed", windowed), + newSettingRow("Resolution (fullscreen)", displayMode), + ) + buttons := &alui.StackPanel{Orientation: alui.OrientationHorizontal} + buttons.AddChild( + newWideButton("Apply", func() { + var controls = controls{MoveLeft: keyLeft.Key, MoveUp: keyUp.Key, MoveRight: keyRight.Key, MoveDown: keyDown.Key} + var video = video{Windowed: windowed.Selected, DisplayMode: displayMode.Mode()} + s.ctx.Settings = settings{Controls: controls, Video: video} + err := s.ctx.Settings.StoreDefault() + if err != nil { + log.Printf("User settings are not stored: %v", err) + } else { + log.Printf("Stored new settings.") + } + + s.ctx.Navigation.showMainMenu() + }), + newWideButton("Cancel", func() { s.ctx.Navigation.showMainMenu() }), + ) + s.AddChild(alui.Center(buttons)) + return nil +} + +func (s *changeSettings) Leave() { +} diff --git a/cmd/krampus19/context.go b/cmd/krampus19/context.go index 2a36498..b10206a 100644 --- a/cmd/krampus19/context.go +++ b/cmd/krampus19/context.go @@ -3,6 +3,7 @@ package main import ( "time" + "opslag.de/schobers/geom" "opslag.de/schobers/krampus19/alui" "opslag.de/schobers/allg5" @@ -20,12 +21,14 @@ func newTexture(bmp *allg5.Bitmap) texture { } type Context struct { - Resources vfs.CopyDir - Textures map[string]texture - Levels map[string]level - Sprites map[string]sprite - Settings settings - Palette *alui.Palette + DisplaySize geom.Point + Resources vfs.CopyDir + Textures map[string]texture + Levels map[string]level + Sprites map[string]sprite + SpriteDrawer SpriteDrawer + Settings settings + Palette *alui.Palette Tick time.Duration Navigation navigation diff --git a/cmd/krampus19/game.go b/cmd/krampus19/game.go index 2734e4d..4823049 100644 --- a/cmd/krampus19/game.go +++ b/cmd/krampus19/game.go @@ -8,6 +8,7 @@ import ( "opslag.de/schobers/allg5" "opslag.de/schobers/fs/vfs" + "opslag.de/schobers/geom" "opslag.de/schobers/krampus19/alui" "opslag.de/schobers/krampus19/gut" ) @@ -141,6 +142,8 @@ func (g *game) loadAssets() error { "tile_lava_brick.png": "lava_brick", "tile_magma.png": "magma", + + "ui.png": "ui", }) if err != nil { return err @@ -162,7 +165,7 @@ func (g *game) loadAssets() error { log.Printf("Loaded %d fonts.\n", g.ui.Fonts().Len()) log.Println("Loading sprites") - err = g.loadSprites("brick", "lava_brick", "magma", "main_character") + err = g.loadSprites("brick", "lava_brick", "magma", "main_character", "ui") if err != nil { return err } @@ -175,9 +178,11 @@ func (g *game) 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, settings settings, res vfs.CopyDir, cons *gut.Console, fps *gut.FPS) error { log.Print("Initializing game...") - g.ctx = &Context{Resources: res, Textures: map[string]texture{}, Settings: newDefaultSettings(), Navigation: navigation{game: g}} + g.ctx = &Context{Resources: res, Textures: map[string]texture{}, Settings: settings, Navigation: navigation{game: g}} + g.ctx.DisplaySize = geom.Pt(disp.Width(), disp.Height()) + g.ctx.SpriteDrawer.ctx = g.ctx if err := g.initUI(disp, cons, fps); err != nil { return err } diff --git a/cmd/krampus19/krampus19.go b/cmd/krampus19/krampus19.go index d57437f..8c7e198 100644 --- a/cmd/krampus19/krampus19.go +++ b/cmd/krampus19/krampus19.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "log" "time" @@ -10,6 +11,7 @@ import ( "opslag.de/schobers/allg5" "opslag.de/schobers/fs/ricefs" "opslag.de/schobers/fs/vfs" + "opslag.de/schobers/geom" "opslag.de/schobers/krampus19/gut" ) @@ -33,14 +35,39 @@ func run() error { cons := &gut.Console{} log.SetOutput(io.MultiWriter(log.Writer(), cons)) - log.Printf("Initializing Allegro") + log.Printf("Initializing Allegro.") err := allg5.Init(allg5.InitAll) if err != nil { return err } - log.Printf("Creating display") - disp, err := allg5.NewDisplay(1440, 900, allg5.NewDisplayOptions{Maximized: false, Windowed: true, Resizable: true, Vsync: true}) + settings := newDefaultSettings() + err = settings.LoadDefault() + if err != nil { + log.Printf("Unable to load settings, falling back on defaults.") + } + err = settings.StoreDefault() + if err != nil { + log.Printf("Unable to store settings.") + } + + log.Printf("Creating display.") + var size = geom.Pt(1280, 720) + dispOptions := allg5.NewDisplayOptions{Vsync: true} + if settings.Video.Windowed { + dispOptions.Maximized = true + dispOptions.Frameless = true + dispOptions.Windowed = true + } else { + dispOptions.Fullscreen = true + str := func(m allg5.DisplayMode) string { return fmt.Sprintf("%d x %d", m.Width, m.Height) } + for _, mode := range allg5.DisplayModes() { + if str(mode) == settings.Video.DisplayMode { + size = geom.Pt(mode.Width, mode.Height) + } + } + } + disp, err := allg5.NewDisplay(size.X, size.Y, dispOptions) if err != nil { return err } @@ -66,7 +93,7 @@ func run() error { defer fps.Destroy() game := &game{} - err = game.Init(disp, res, cons, fps) + err = game.Init(disp, settings, res, cons, fps) if err != nil { return err } @@ -94,6 +121,8 @@ func run() error { log.Printf("Stopping game loop, user pressed Alt+F4") return nil } + case *allg5.DisplayResizeEvent: + game.ctx.DisplaySize = geom.Pt(disp.Width(), disp.Height()) } game.Handle(e) e = eq.Get() diff --git a/cmd/krampus19/mainmenu.go b/cmd/krampus19/mainmenu.go index 1a0e69b..19c2ef1 100644 --- a/cmd/krampus19/mainmenu.go +++ b/cmd/krampus19/mainmenu.go @@ -14,6 +14,7 @@ func (m *mainMenu) Enter(ctx *Context) error { m.ctx = ctx m.Init() m.Add("Play", func() { m.ctx.Navigation.playLevel("1") }) + m.Add("Settings", func() { m.ctx.Navigation.changeSettings() }) m.Add("Quit", func() { m.ctx.Navigation.quit() }) return nil } diff --git a/cmd/krampus19/navigation.go b/cmd/krampus19/navigation.go index ede6605..bb5ee6c 100644 --- a/cmd/krampus19/navigation.go +++ b/cmd/krampus19/navigation.go @@ -16,6 +16,10 @@ type scene interface { Leave() } +func (n *navigation) changeSettings() { + n.switchTo(&changeSettings{}) +} + func (n *navigation) playLevel(l string) { n.switchTo(&playLevel{name: l}) } @@ -34,6 +38,7 @@ func (n *navigation) switchTo(s scene) { } n.curr = s n.game.scene.Proxy = s + n.game.scene.Visible = s != nil if n.curr != nil { n.curr.Enter(n.game.ctx) } diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index fc4907e..7adada7 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -165,32 +165,12 @@ func (l *playLevel) Handle(e allg5.Event) { } } -func (l *playLevel) drawSpritePart(name, partName string, pos geom.PointF32) { - l.drawSpritePartOffset(name, partName, pos, 0) +func (l *playLevel) drawSprite(name, partName string, pos geom.PointF32) { + l.drawSpritePart(name, partName, pos, 0) } -func (l *playLevel) drawSpritePartOffset(name, partName string, pos geom.PointF32, z float32) { - sprite, ok := l.ctx.Sprites[name] - if !ok { - return - } - text, ok := l.ctx.Textures[sprite.texture] - if !ok { - return - } - partText, ok := text.Subs[partName] - if !ok { - return - } - part := sprite.FindPartByName(partName) - scale := l.scale - if part.scale != 0 { - scale *= 1. / part.scale - } - anchor := part.sub.Min.Sub(part.anchor).ToF32().Mul(scale) - scrPos := l.posToScreenF32(pos, z).Add(anchor) - left, top := scrPos.X, scrPos.Y - partText.DrawOptions(left, top, allg5.DrawOptions{Scale: allg5.NewUniformScale(scale)}) +func (l *playLevel) drawSpritePart(name, partName string, pos geom.PointF32, z float32) { + l.ctx.SpriteDrawer.Draw(name, partName, l.posToScreenF32(pos, z), DrawSpriteOptions{Scale: l.scale}) } func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { @@ -200,15 +180,15 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { switch t { case tileBasic: if l.state.IsNextToMagma(pos) { - l.drawSpritePart("lava_brick", "magma", pos.ToF32()) + l.drawSprite("lava_brick", "magma", pos.ToF32()) } else { - l.drawSpritePart("lava_brick", "lava_brick", pos.ToF32()) + l.drawSprite("lava_brick", "lava_brick", pos.ToF32()) } case tileMagma: - l.drawSpritePart("magma", "magma", pos.ToF32()) + l.drawSprite("magma", "magma", pos.ToF32()) if l.state.IsFilledUp(pos) { - l.drawSpritePartOffset("brick", "brick", pos.ToF32(), 80) - l.drawSpritePart("magma", "sunken_overlay", pos.ToF32()) + l.drawSpritePart("brick", "brick", pos.ToF32(), 80) + l.drawSprite("magma", "sunken_overlay", pos.ToF32()) } } } @@ -224,9 +204,9 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { for _, e := range entities { switch e.typ { case entityTypeCharacter: - l.drawSpritePart("main_character", "main_character", e.scr) + l.drawSprite("main_character", "main_character", e.scr) case entityTypeBrick: - l.drawSpritePart("brick", "brick", e.scr) + l.drawSprite("brick", "brick", e.scr) } } diff --git a/cmd/krampus19/res/sprites/ui.txt b/cmd/krampus19/res/sprites/ui.txt new file mode 100644 index 0000000..4ad87b9 --- /dev/null +++ b/cmd/krampus19/res/sprites/ui.txt @@ -0,0 +1,32 @@ +sprite: +texture: ui + +part: +name: square +sub_texture: 0,0,448,512 +anchor: 224,256 +scale: 16 +:part + +part: +name: check-square +sub_texture: 0,512,448,512 +anchor: 224,768 +scale: 16 +:part + +part: +name: angle-left +sub_texture: 768,0,192,512 +anchor: 848,196 +scale: 16 +:part + +part: +name: angle-right +sub_texture: 768,512,192,512 +anchor: 848,720 +scale: 16 +:part + +:sprite \ No newline at end of file diff --git a/cmd/krampus19/res/ui.png b/cmd/krampus19/res/ui.png new file mode 100644 index 0000000000000000000000000000000000000000..63ec3b363a19660c904e9d64497f10b8320ba7ec GIT binary patch literal 34252 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 literal 0 HcmV?d00001 diff --git a/cmd/krampus19/settings.go b/cmd/krampus19/settings.go index 13b2440..35ceb4f 100644 --- a/cmd/krampus19/settings.go +++ b/cmd/krampus19/settings.go @@ -1,10 +1,12 @@ package main -import "opslag.de/schobers/allg5" +import ( + "encoding/json" + "os" + "path/filepath" -type settings struct { - Controls controls -} + "opslag.de/schobers/allg5" +) func newDefaultSettings() settings { return settings{ @@ -14,9 +16,71 @@ func newDefaultSettings() settings { MoveDown: allg5.KeyDown, MoveLeft: allg5.KeyLeft, }, + Video: video{ + Windowed: true, + DisplayMode: "", + }, } } +type settings struct { + Controls controls + Video video +} + +func (s *settings) DefaultPath() (string, error) { + config, err := os.UserConfigDir() + if err != nil { + return "", err + } + dir := filepath.Join(config, "krampus19") + err = os.MkdirAll(dir, 0600) + if err != nil { + return "", err + } + return filepath.Join(dir, "settings.json"), nil +} + +func (s *settings) Load(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + var fromFile settings + err = json.NewDecoder(f).Decode(&fromFile) + if err != nil { + return err + } + *s = fromFile + return nil +} + +func (s *settings) LoadDefault() error { + path, err := s.DefaultPath() + if err != nil { + return err + } + return s.Load(path) +} + +func (s *settings) Store(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(&s) +} + +func (s *settings) StoreDefault() error { + path, err := s.DefaultPath() + if err != nil { + return err + } + return s.Store(path) +} + type controls struct { MoveUp allg5.Key MoveRight allg5.Key @@ -24,6 +88,11 @@ type controls struct { MoveLeft allg5.Key } +type video struct { + Windowed bool + DisplayMode string +} + func (c controls) MovementKeys() []allg5.Key { return []allg5.Key{c.MoveUp, c.MoveRight, c.MoveDown, c.MoveLeft} } diff --git a/cmd/krampus19/spritedrawer.go b/cmd/krampus19/spritedrawer.go new file mode 100644 index 0000000..43bd826 --- /dev/null +++ b/cmd/krampus19/spritedrawer.go @@ -0,0 +1,69 @@ +package main + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +type DrawSpriteOptions struct { + Scale float32 + Tint *allg5.Color +} + +type SpriteDrawer struct { + ctx *Context +} + +func (d SpriteDrawer) scale(sprite, user float32) float32 { + var scale float32 = 1 + if sprite != 0 { + scale *= 1 / sprite + } + if user != 0 { + scale *= user + } + return scale +} + +func (d SpriteDrawer) Draw(name, partName string, pos geom.PointF32, opts DrawSpriteOptions) bool { + sprite, ok := d.ctx.Sprites[name] + if !ok { + return false + } + text, ok := d.ctx.Textures[sprite.texture] + if !ok { + return false + } + partText, ok := text.Subs[partName] + if !ok { + return false + } + part := sprite.FindPartByName(partName) + scale := d.scale(part.scale, opts.Scale) + anchor := part.sub.Min.Sub(part.anchor).ToF32().Mul(scale) + scrPos := pos.Add(anchor) + left, top := scrPos.X, scrPos.Y + var drawOpts allg5.DrawOptions + if scale != 1 { + drawOpts.Scale = allg5.NewUniformScale(scale) + } + if opts.Tint != nil { + drawOpts.Tint = opts.Tint + } + partText.DrawOptions(left, top, drawOpts) + return true +} + +func (d SpriteDrawer) Size(name, partName string) geom.PointF32 { + return d.SizeScale(name, partName, 0) +} + +func (d SpriteDrawer) SizeScale(name, partName string, scale float32) geom.PointF32 { + sprite, ok := d.ctx.Sprites[name] + if !ok { + return geom.PointF32{} + } + part := sprite.FindPartByName(partName) + scale = d.scale(part.scale, scale) + return part.sub.Size().ToF32().Mul(scale) +}