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.
This commit is contained in:
Sander Schobers 2019-12-28 16:03:57 +01:00
parent 27ad0453c3
commit 11ab3fca0f
19 changed files with 641 additions and 80 deletions

View File

@ -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)

23
alui/center.go Normal file
View File

@ -0,0 +1,23 @@
package alui
import (
"opslag.de/schobers/geom"
)
type center struct {
Proxy
}
func Center(control Control) Control {
return &center{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))
}

34
alui/column.go Normal file
View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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() {
}

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

BIN
cmd/krampus19/res/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -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}
}

View File

@ -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)
}