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 0000000..63ec3b3 Binary files /dev/null and b/cmd/krampus19/res/ui.png differ 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) +}