Settings Window + Bugfix

* Added a Settings window for game options, such as enabling the
  horizontal toolbars in Edit Mode. The Settings window also has a
  Controls tab showing the gameplay buttons and keyboard shortcuts.
* The Settings window is available as a button on the home screen OR
  from the Edit->Settings menu in the EditScene.
* Bugfix: using WASD to move the player character now works better and
  is considered by the game to be identical to the arrow key inputs. Boy
  now updates his animation based on these keys, and they register as
  boolean on/off keys instead of affected by key-repeat.
* Refactor the boolProps: they are all part of usercfg now, and if you
  run e.g. "boolProp show-all-doodads true" and then cause the user
  settings to save to disk, that boolProp will be permanently enabled
  until turned off again.
This commit is contained in:
Noah 2021-06-19 22:14:41 -07:00
parent d0cfa50625
commit 864156da53
21 changed files with 762 additions and 120 deletions

View File

@ -3,7 +3,6 @@ package main
import (
"fmt"
"log"
"os"
"sort"
"time"
@ -11,6 +10,7 @@ import (
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli/v2"
)
@ -63,6 +63,7 @@ func main() {
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
log.Error("Fatal: %s", err)
os.Exit(1)
}
}

View File

@ -19,6 +19,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/sound"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/render/sdl"
"github.com/urfave/cli/v2"
@ -56,6 +57,11 @@ func main() {
freeLabel = " (shareware)"
}
// Load user settings from disk ASAP.
if err := usercfg.Load(); err != nil {
log.Error("Error loading user settings (defaults will be used): %s", err)
}
app.Version = fmt.Sprintf("%s build %s%s. Built on %s",
branding.Version,
Build,
@ -194,7 +200,9 @@ func setResolution(value string) error {
case "mobile":
balance.Width = 375
balance.Height = 812
balance.HorizontalToolbars = true
if !usercfg.Current.Initialized {
usercfg.Current.HorizontalToolbars = true
}
case "landscape":
balance.Width = 812
balance.Height = 375

View File

@ -5,37 +5,42 @@ import (
"fmt"
"sort"
"strings"
"git.kirsle.net/apps/doodle/pkg/usercfg"
)
// Fun bool props to wreak havoc in the game.
var (
// Force show hidden doodads in the palette in Editor Mode.
ShowHiddenDoodads bool
/*
Boolprop is a boolean setting that can be toggled in the game using the
developer console. Many of these consist of usercfg settings that are
not exposed to the Settings UI window, and secret testing functions.
Where one points to usercfg, check usercfg.Settings for documentation
about what that boolean does.
*/
type Boolprop struct {
Name string
Get func() bool
Set func(bool)
}
// Force ability to edit Locked levels and doodads.
WriteLockOverride bool
// Pretty-print JSON files when writing.
JSONIndent bool
// Temporary debug flag.
TempDebug bool
// Draw horizontal toolbars in Level Editor instead of vertical.
HorizontalToolbars bool
)
// Human friendly names for the boolProps. Not necessarily the long descriptive
// variable names above.
var props = map[string]*bool{
"showAllDoodads": &ShowHiddenDoodads,
"writeLockOverride": &WriteLockOverride,
"prettyJSON": &JSONIndent,
"tempDebug": &TempDebug,
"horizontalToolbars": &HorizontalToolbars,
// WARNING: SLOW!
"disableChunkTextureCache": &DisableChunkTextureCache,
// Boolprops are the map of available boolprops, shown in the dev
// console when you type: "boolProp list"
var Boolprops = map[string]Boolprop{
"show-hidden-doodads": {
Get: func() bool { return usercfg.Current.ShowHiddenDoodads },
Set: func(v bool) { usercfg.Current.ShowHiddenDoodads = v },
},
"write-lock-override": {
Get: func() bool { return usercfg.Current.WriteLockOverride },
Set: func(v bool) { usercfg.Current.WriteLockOverride = v },
},
"pretty-json": {
Get: func() bool { return usercfg.Current.JSONIndent },
Set: func(v bool) { usercfg.Current.JSONIndent = v },
},
"horizontal-toolbars": {
Get: func() bool { return usercfg.Current.HorizontalToolbars },
Set: func(v bool) { usercfg.Current.HorizontalToolbars = v },
},
}
// GetBoolProp reads the current value of a boolProp.
@ -43,25 +48,25 @@ var props = map[string]*bool{
func GetBoolProp(name string) (bool, error) {
if name == "list" {
var keys []string
for k := range props {
for k := range Boolprops {
keys = append(keys, k)
}
sort.Strings(keys)
return false, fmt.Errorf(
"Boolprops: %s",
"boolprops: %s",
strings.Join(keys, ", "),
)
}
if prop, ok := props[name]; ok {
return *prop, nil
if prop, ok := Boolprops[name]; ok {
return prop.Get(), nil
}
return false, errors.New("no such boolProp")
}
// BoolProp allows easily setting a boolProp by name.
func BoolProp(name string, v bool) error {
if prop, ok := props[name]; ok {
*prop = v
if prop, ok := Boolprops[name]; ok {
prop.Set(v)
return nil
}
return errors.New("no such boolProp")

View File

@ -77,6 +77,14 @@ var (
Color: render.Black,
}
// CodeLiteralFont for rendering <code>-like text.
CodeLiteralFont = render.Text{
Size: 11,
PadX: 3,
FontFilename: "DejaVuSansMono.ttf",
Color: render.Magenta,
}
// Small font
SmallFont = render.Text{
Size: 10,

View File

@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/bindata"
@ -34,7 +35,7 @@ func (c Command) Run(d *Doodle) error {
return nil
}
switch c.Command {
switch strings.ToLower(c.Command) {
case "echo":
d.Flash(c.ArgsLiteral)
return nil
@ -76,7 +77,7 @@ func (c Command) Run(d *Doodle) error {
case "repl":
d.shell.Repl = true
d.shell.Text = "$ "
case "boolProp":
case "boolprop":
return c.BoolProp(d)
case "extract-bindata":
// Undocumented command to extract the binary of its assets.
@ -150,7 +151,7 @@ func (c Command) Help(d *Doodle) error {
return nil
}
switch c.Args[0] {
switch strings.ToLower(c.Args[0]) {
case "echo":
d.Flash("Usage: echo <message>")
d.Flash("Flash a message back to the console")
@ -183,7 +184,7 @@ func (c Command) Help(d *Doodle) error {
d.Flash("Evaluate a line of JavaScript on the in-game interpreter")
case "repl":
d.Flash("Enter a JavaScript shell on the in-game interpreter")
case "boolProp":
case "boolprop":
d.Flash("Toggle boolean values. `boolProp list` lists available")
case "help":
d.Flash("Usage: help <command>")

View File

@ -8,14 +8,14 @@ import (
"os"
"path/filepath"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/usercfg"
)
// ToJSON serializes the doodad as JSON.
func (d *Doodad) ToJSON() ([]byte, error) {
out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out)
if balance.JSONIndent {
if usercfg.Current.JSONIndent {
encoder.SetIndent("", "\t")
}
err := encoder.Encode(d)

View File

@ -15,9 +15,12 @@ import (
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/pattern"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/windows"
golog "git.kirsle.net/go/log"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
)
const (
@ -208,13 +211,32 @@ func (d *Doodle) Run() error {
d.TrackFPS(delay)
// Consume any lingering key sym.
ev.ResetKeyDown()
// ev.ResetKeyDown()
}
log.Warn("Main Loop Exited! Shutting down...")
return nil
}
// MakeSettingsWindow initializes the windows/settings.go window
// from anywhere you need it, binding all the variables in.
func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window {
cfg := windows.Settings{
Supervisor: supervisor,
Engine: d.Engine,
SceneName: d.Scene.Name(),
OnApply: func() {
},
// Boolean checkbox bindings
DebugOverlay: &DebugOverlay,
DebugCollision: &DebugCollision,
HorizontalToolbars: &usercfg.Current.HorizontalToolbars,
}
return windows.MakeSettingsWindow(d.width, d.height, cfg)
}
// ConfirmExit may shut down Doodle gracefully after showing the user a
// confirmation modal.
func (d *Doodle) ConfirmExit() {

View File

@ -15,6 +15,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
@ -93,7 +94,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
// Write locked level?
if s.Level != nil && s.Level.Locked {
if balance.WriteLockOverride {
if usercfg.Current.WriteLockOverride {
d.Flash("Note: write lock has been overridden")
} else {
d.Flash("That level is write-protected and cannot be viewed in the editor.")
@ -123,7 +124,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
// Write locked doodad?
if s.Doodad != nil && s.Doodad.Locked {
if balance.WriteLockOverride {
if usercfg.Current.WriteLockOverride {
d.Flash("Note: write lock has been overridden")
} else {
d.Flash("That doodad is write-protected and cannot be viewed in the editor.")

View File

@ -12,6 +12,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
@ -50,6 +51,7 @@ type EditorUI struct {
publishWindow *ui.Window
filesystemWindow *ui.Window
licenseWindow *ui.Window
settingsWindow *ui.Window // lazy loaded
// Palette window.
Palette *ui.Window
@ -158,7 +160,7 @@ func (u *EditorUI) Resized(d *Doodle) {
// Palette panel.
{
if balance.HorizontalToolbars {
if usercfg.Current.HorizontalToolbars {
u.Palette.Configure(ui.Config{
Width: u.d.width,
Height: paletteWidth,
@ -191,7 +193,7 @@ func (u *EditorUI) Resized(d *Doodle) {
Width: toolbarWidth,
Height: innerHeight,
}
if balance.HorizontalToolbars {
if usercfg.Current.HorizontalToolbars {
tbSize.Width = innerWidth
tbSize.Height = toolbarWidth
}
@ -207,7 +209,7 @@ func (u *EditorUI) Resized(d *Doodle) {
{
frame := u.Workspace
if balance.HorizontalToolbars {
if usercfg.Current.HorizontalToolbars {
frame.MoveTo(render.NewPoint(
0,
menuHeight+u.ToolBar.Size().H,

View File

@ -105,6 +105,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
editMenu.AddItemAccel("Redo", "Ctrl-Y", func() {
u.Canvas.RedoStroke()
})
editMenu.AddSeparator()
editMenu.AddItem("Settings", func() {
if u.settingsWindow == nil {
u.settingsWindow = d.MakeSettingsWindow(u.Supervisor)
}
u.settingsWindow.Show()
})
////////
// Level menu

View File

@ -5,6 +5,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/ui"
)
@ -48,7 +49,7 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
tooltipEdge = ui.Left
buttonSize = 32
)
if balance.HorizontalToolbars {
if usercfg.Current.HorizontalToolbars {
packAlign = ui.W
packConfig = ui.Pack{
Side: packAlign,

View File

@ -5,6 +5,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/sprites"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
@ -18,7 +19,7 @@ var toolbarSpriteSize = 32 // 32x32 sprites.
func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
// Horizontal toolbar instead of vertical?
var (
isHoz = balance.HorizontalToolbars
isHoz = usercfg.Current.HorizontalToolbars
packAlign = ui.N
frameSize = render.NewRect(toolbarWidth, 100)
tooltipEdge = ui.Right

View File

@ -9,6 +9,80 @@ package keybind
import "git.kirsle.net/go/render/event"
// State returns a version of event.State which is domain specific
// to what the game actually cares about.
type State struct {
State *event.State
Shutdown bool // Escape key
Help bool // F1
DebugOverlay bool // F3
DebugCollision bool // F4
Undo bool // Ctrl-Z
Redo bool // Ctrl-Y
NewLevel bool // Ctrl-N
Save bool // Ctrl-S
SaveAs bool // Shift-Ctrl-S
Open bool // Ctrl-O
ZoomIn bool // +
ZoomOut bool // -
ZoomReset bool // 1
Origin bool // 0
GotoPlay bool // p
GotoEdit bool // e
PencilTool bool
LineTool bool
RectTool bool
EllipseTool bool
EraserTool bool
DoodadDropper bool
ShellKey bool
Enter bool
Left bool
Right bool
Up bool
Down bool
Use bool
}
// FromEvent converts a render.Event readout of the current keys
// being pressed but formats them in the way the game uses them.
// For example, WASD and arrow keys both move the player and the
// game only cares which direction.
func FromEvent(ev *event.State) State {
return State{
State: ev,
Shutdown: Shutdown(ev),
Help: Help(ev),
DebugOverlay: DebugOverlay(ev),
DebugCollision: DebugCollision(ev), // F4
Undo: Undo(ev), // Ctrl-Z
Redo: Redo(ev), // Ctrl-Y
NewLevel: NewLevel(ev), // Ctrl-N
Save: Save(ev), // Ctrl-S
SaveAs: SaveAs(ev), // Shift-Ctrl-S
Open: Open(ev), // Ctrl-O
ZoomIn: ZoomIn(ev), // +
ZoomOut: ZoomOut(ev), // -
ZoomReset: ZoomReset(ev), // 1
Origin: Origin(ev), // 0
GotoPlay: GotoPlay(ev), // p
GotoEdit: GotoEdit(ev), // e
PencilTool: PencilTool(ev),
LineTool: LineTool(ev),
RectTool: RectTool(ev),
EllipseTool: EllipseTool(ev),
EraserTool: EraserTool(ev),
DoodadDropper: DoodadDropper(ev),
ShellKey: ShellKey(ev),
Enter: Enter(ev),
Left: Left(ev),
Right: Right(ev),
Up: Up(ev),
Down: Down(ev),
Use: Use(ev),
}
}
// Shutdown (Escape) signals the game to start closing down.
func Shutdown(ev *event.State) bool {
return ev.Escape

View File

@ -7,7 +7,7 @@ import (
"io/ioutil"
"os"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/usercfg"
)
// FromJSON loads a level from JSON string.
@ -34,7 +34,7 @@ func FromJSON(filename string, data []byte) (*Level, error) {
func (m *Level) ToJSON() ([]byte, error) {
out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out)
if balance.JSONIndent {
if usercfg.Current.JSONIndent {
encoder.SetIndent("", "\t")
}
err := encoder.Encode(m)

View File

@ -34,6 +34,7 @@ type MainScene struct {
frame *ui.Frame // Main button frame
btnRegister *ui.Button
winRegister *ui.Window
winSettings *ui.Window
// Update check variables.
updateButton *ui.Button
@ -169,10 +170,15 @@ func (s *MainScene) Setup(d *Doodle) error {
Name: "Edit a Level",
Func: d.GotoLoadMenu,
},
// {
// Name: "Settings",
// Func: d.GotoSettingsMenu,
// },
{
Name: "Settings",
Func: func() {
if s.winSettings == nil {
s.winSettings = d.MakeSettingsWindow(s.Supervisor)
}
s.winSettings.Show()
},
},
}
for _, button := range buttons {
button := button
@ -345,13 +351,20 @@ func (s *MainScene) Draw(d *Doodle) error {
s.canvas.Present(d.Engine, render.Origin)
// Draw a sheen over the level for clarity.
d.Engine.DrawBox(render.RGBA(255, 255, 254, 128), render.Rect{
d.Engine.DrawBox(render.RGBA(255, 255, 254, 96), render.Rect{
X: 0,
Y: 0,
W: d.width,
H: d.height,
})
// Draw out bounding boxes.
if DebugCollision {
for _, actor := range s.canvas.Actors() {
d.DrawCollisionBox(s.canvas, actor)
}
}
// App title label.
s.labelTitle.MoveTo(render.Point{
X: (d.width / 2) - (s.labelTitle.Size().W / 2),

View File

@ -551,7 +551,7 @@ func (s *PlayScene) movePlayer(ev *event.State) {
// If the "Use" key is pressed, set an actor flag on the player.
s.Player.SetUsing(keybind.Use(ev))
s.scripting.To(s.Player.ID()).Events.RunKeypress(ev)
s.scripting.To(s.Player.ID()).Events.RunKeypress(keybind.FromEvent(ev))
}
// Drawing returns the private world drawing, for debugging with the console.

View File

@ -4,7 +4,7 @@ import (
"errors"
"sync"
"git.kirsle.net/go/render/event"
"git.kirsle.net/apps/doodle/pkg/keybind"
"github.com/robertkrimen/otto"
)
@ -73,7 +73,7 @@ func (e *Events) OnKeypress(call otto.FunctionCall) otto.Value {
}
// RunKeypress invokes the OnCollide handler function.
func (e *Events) RunKeypress(ev *event.State) error {
func (e *Events) RunKeypress(ev keybind.State) error {
return e.run(KeypressEvent, ev)
}

97
pkg/usercfg/usercfg.go Normal file
View File

@ -0,0 +1,97 @@
/*
Package usercfg has functions around the user's Game Settings.
Other places in the codebase to look for its related functionality:
- pkg/windows/settings.go: the Settings Window is the UI owner of
this feature, it adjusts the usercfg.Current struct and Saves the
changes to disk.
*/
package usercfg
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"time"
"git.kirsle.net/apps/doodle/pkg/userdir"
)
// Settings are the available game settings.
type Settings struct {
// Initialized is set true the first time the settings are saved to
// disk, so the game may decide some default settings for first-time
// user experience, e.g. set horizontal toolbars for mobile.
Initialized bool
// Configurable settings (pkg/windows/settings.go)
HorizontalToolbars bool `json:",omitempty"`
// Secret boolprops from balance/boolprops.go
ShowHiddenDoodads bool `json:",omitempty"`
WriteLockOverride bool `json:",omitempty"`
JSONIndent bool `json:",omitempty"`
// Bookkeeping.
UpdatedAt time.Time
}
// Current loaded settings, good defaults by default.
var Current = Defaults()
// Defaults returns sensible default user settings.
func Defaults() *Settings {
return &Settings{}
}
// Filepath returns the path to the settings file.
func Filepath() string {
return filepath.Join(userdir.ProfileDirectory, "settings.json")
}
// Save the settings to disk.
func Save() error {
var (
filename = Filepath()
bin = bytes.NewBuffer([]byte{})
enc = json.NewEncoder(bin)
)
enc.SetIndent("", "\t")
Current.Initialized = true
Current.UpdatedAt = time.Now()
if err := enc.Encode(Current); err != nil {
return err
}
err := ioutil.WriteFile(filename, bin.Bytes(), 0644)
return err
}
// Load the settings from disk. The loaded settings will be available
// at usercfg.Current.
func Load() error {
var (
filename = Filepath()
settings = Defaults()
)
if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil // no file, no problem
}
fh, err := os.Open(filename)
if err != nil {
return err
}
// Decode JSON from file.
dec := json.NewDecoder(fh)
err = dec.Decode(settings)
if err != nil {
return err
}
Current = settings
return nil
}

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
@ -81,7 +82,7 @@ func NewDoodadDropper(config DoodadDropper) *ui.Window {
}
// Skip hidden doodads.
if doodad.Hidden && !balance.ShowHiddenDoodads {
if doodad.Hidden && !usercfg.Current.ShowHiddenDoodads {
continue
}

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/pattern"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
@ -226,7 +227,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
})
for _, t := range pattern.Builtins {
if t.Hidden && !balance.ShowHiddenDoodads {
if t.Hidden && !usercfg.Current.ShowHiddenDoodads {
continue
}

View File

@ -1,13 +1,14 @@
package windows
import (
"fmt"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
)
// Settings window.
@ -15,80 +16,478 @@ type Settings struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
// Boolean bindings.
DebugOverlay *bool
DebugCollision *bool
HorizontalToolbars *bool
// Configuration options.
SceneName string // name of scene which called this window
ActiveTab string // specify the tab to open
OnApply func()
}
// MakeSettingsWindow initializes a settings window for any scene.
// The window width/height are the actual SDL2 window dimensions.
func MakeSettingsWindow(windowWidth, windowHeight int, cfg Settings) *ui.Window {
win := NewSettingsWindow(cfg)
win.Compute(cfg.Engine)
win.Supervise(cfg.Supervisor)
// Center the window.
size := win.Size()
win.MoveTo(render.Point{
X: (windowWidth / 2) - (size.W / 2),
Y: (windowHeight / 2) - (size.H / 2),
})
return win
}
// NewSettingsWindow initializes the window.
func NewSettingsWindow(cfg Settings) *ui.Window {
window := ui.NewWindow("Game Settings")
var (
Width = 400
Height = 400
ActiveTab = "index"
// The tab frames
TabOptions *ui.Frame // index
TabControls *ui.Frame // controls
)
if cfg.ActiveTab != "" {
ActiveTab = cfg.ActiveTab
}
window := ui.NewWindow("Settings")
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: 400,
Height: 170,
Width: Width,
Height: Height,
Background: render.Grey,
})
// {
// row := ui.NewFrame("Theme Frame")
// label := ui.NewLabel(ui.Label{
// Text: "Theme:",
// Font: balance.MenuFont,
// })
// }
text := ui.NewLabel(ui.Label{
Text: fmt.Sprintf("%s is a drawing-based maze game.\n\n"+
"Copyright © %s.\nAll rights reserved.\n\n"+
"Version %s",
branding.AppName,
branding.Copyright,
branding.Version,
),
///////////
// Tab Bar
tabFrame := ui.NewFrame("Tab Bar")
tabFrame.SetBackground(render.DarkGrey)
window.Pack(tabFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
window.Pack(text, ui.Pack{
Side: ui.N,
Padding: 8,
})
frame := ui.NewFrame("Button frame")
buttons := []struct {
for _, tab := range []struct {
label string
f func()
value string
}{
{"Website", func() {
native.OpenURL(branding.Website)
}},
{"Open Source Licenses", func() {
// TODO: open file
native.OpenURL("./Open Source Licenses.md")
}},
}
for _, button := range buttons {
button := button
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
Text: button.label,
Font: balance.MenuFont,
{"Options", "index"},
{"Controls", "controls"},
} {
radio := ui.NewRadioButton(tab.label, &ActiveTab, tab.value, ui.NewLabel(ui.Label{
Text: tab.label,
Font: balance.UIFont,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.f()
radio.SetStyle(&balance.ButtonBabyBlue)
radio.Handle(ui.Click, func(ed ui.EventData) error {
switch ActiveTab {
case "index":
TabOptions.Show()
TabControls.Hide()
case "controls":
TabOptions.Hide()
TabControls.Show()
}
return nil
})
btn.Compute(cfg.Engine)
cfg.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
cfg.Supervisor.Add(radio)
tabFrame.Pack(radio, ui.Pack{
Side: ui.W,
PadX: 4,
Expand: true,
Fill: true,
})
}
window.Pack(frame, ui.Pack{
Side: ui.N,
Padding: 8,
///////////
// Options (index) Tab
TabOptions = cfg.makeOptionsTab(Width, Height)
if ActiveTab != "index" {
TabOptions.Hide()
}
window.Pack(TabOptions, ui.Pack{
Side: ui.N,
FillX: true,
})
///////////
// Controls Tab
TabControls = cfg.makeControlsTab(Width, Height)
if ActiveTab != "controls" {
TabControls.Hide()
}
window.Pack(TabControls, ui.Pack{
Side: ui.N,
FillX: true,
})
return window
}
// saveGameSettings controls pkg/usercfg to write the user settings
// to disk, based on the settings toggle-able from the UI window.
func saveGameSettings() {
log.Info("Saving game settings")
if err := usercfg.Save(); err != nil {
log.Error("Couldn't save game settings: %s", err)
}
}
// Settings Window "Options" Tab
func (c Settings) makeOptionsTab(Width, Height int) *ui.Frame {
tab := ui.NewFrame("Options Tab")
log.Error("c.Super: %+v", c.Supervisor)
// Common click handler for all settings,
// so we can write the updated info to disk.
onClick := func(ed ui.EventData) error {
saveGameSettings()
return nil
}
rows := []struct {
Header string
Text string
Boolean *bool
TextVariable *string
PadY int
PadX int
name string // for special cases
}{
{
Text: "Notice: all settings are temporary and controls are not editable.",
PadY: 2,
},
{
Header: "Game Options",
},
{
Boolean: c.HorizontalToolbars,
Text: "Editor: Horizontal instead of vertical toolbars",
PadX: 4,
name: "toolbars",
},
{
Header: "Debug Options",
},
{
Boolean: c.DebugOverlay,
Text: "Show debug text overlay (F3)",
PadX: 4,
},
{
Boolean: c.DebugCollision,
Text: "Show collision hitboxes (F4)",
PadX: 4,
},
{
Header: "My Custom Content",
},
{
Text: "Levels and doodads you create in-game are placed in your\n" +
"Profile Directory. This is also where you can place content made\n" +
"by others to use them in your game. Click on the button below\n" +
"to (hopefully) be taken to your Profile Directory:",
},
}
for _, row := range rows {
row := row
frame := ui.NewFrame("Frame")
tab.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: row.PadY,
})
// Headers get their own row to themselves.
if row.Header != "" {
label := ui.NewLabel(ui.Label{
Text: row.Header,
Font: balance.LabelFont,
})
frame.Pack(label, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Checkboxes get their own row.
if row.Boolean != nil {
cb := ui.NewCheckbox(row.Text, row.Boolean, ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
}))
cb.Handle(ui.Click, onClick)
cb.Supervise(c.Supervisor)
// Add warning to the toolbars option if the EditMode is currently active.
if row.name == "toolbars" && c.SceneName == "Edit" {
ui.NewTooltip(cb, ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" +
"Playtesting and returning will do.",
Edge: ui.Top,
})
}
frame.Pack(cb, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Any leftover Text gets packed to the left.
if row.Text != "" {
tf := ui.NewFrame("TextFrame")
label := ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
})
tf.Pack(label, ui.Pack{
Side: ui.W,
})
frame.Pack(tf, ui.Pack{
Side: ui.W,
})
}
}
// Button toolbar.
btnFrame := ui.NewFrame("Button Frame")
tab.Pack(btnFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 4,
})
for _, button := range []struct {
Label string
Fn func()
Style *style.Button
}{
{
Label: "Open profile directory",
Fn: func() {
native.OpenURL("file://" + userdir.ProfileDirectory)
},
Style: &balance.ButtonPrimary,
},
} {
btn := ui.NewButton(button.Label, ui.NewLabel(ui.Label{
Text: button.Label,
Font: balance.UIFont,
}))
if button.Style != nil {
btn.SetStyle(button.Style)
}
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.Fn()
return nil
})
c.Supervisor.Add(btn)
btnFrame.Pack(btn, ui.Pack{
Side: ui.W,
Expand: true,
})
}
return tab
}
// Settings Window "Controls" Tab
func (c Settings) makeControlsTab(Width, Height int) *ui.Frame {
var (
halfWidth = (Width - 4) / 2 // the 4 is for window borders, TODO
shortcutTabWidth = float64(halfWidth) * 0.5
infoTabWidth = float64(halfWidth) * 0.5
rowHeight = 20
shortcutTabSize = render.NewRect(int(shortcutTabWidth), rowHeight)
infoTabSize = render.NewRect(int(infoTabWidth), rowHeight)
)
frame := ui.NewFrame("Controls Tab")
controls := []struct {
Header string
Label string
Shortcut string
}{
{
Header: "Universal Shortcut Keys",
},
{
Shortcut: "Escape",
Label: "Exit game",
},
{
Shortcut: "F1",
Label: "Guidebook",
},
{
Shortcut: "`",
Label: "Dev console",
},
{
Header: "Gameplay Controls (Play Mode)",
},
{
Shortcut: "Up or W",
Label: "Jump",
},
{
Shortcut: "Space",
Label: "Activate",
},
{
Shortcut: "Left or A",
Label: "Move left",
},
{
Shortcut: "Right or D",
Label: "Move right",
},
{
Header: "Level Editor Shortcuts",
},
{
Shortcut: "Ctrl-N",
Label: "New level",
},
{
Shortcut: "Ctrl-O",
Label: "Open drawing",
},
{
Shortcut: "Ctrl-S",
Label: "Save drawing",
},
{
Shortcut: "Shift-Ctrl-S",
Label: "Save a copy",
},
{
Shortcut: "Ctrl-Z",
Label: "Undo stroke",
},
{
Shortcut: "Ctrl-Y",
Label: "Redo stroke",
},
{
Shortcut: "P",
Label: "Playtest",
},
{
Shortcut: "0",
Label: "Scroll to origin",
},
{
Shortcut: "d",
Label: "Doodads",
},
{
Shortcut: "f",
Label: "Pencil Tool",
},
{
Shortcut: "l",
Label: "Line Tool",
},
{
Shortcut: "r",
Label: "Rectangle Tool",
},
{
Shortcut: "c",
Label: "Ellipse Tool",
},
{
Shortcut: "x",
Label: "Eraser Tool",
},
}
var curFrame = ui.NewFrame("Frame")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
var i = -1 // manually controlled
for _, row := range controls {
i++
row := row
if row.Header != "" {
// Close out a previous Frame?
if i != 0 {
curFrame = ui.NewFrame("Header Row")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
}
label := ui.NewLabel(ui.Label{
Text: row.Header,
Font: balance.LabelFont,
})
curFrame.Pack(label, ui.Pack{
Side: ui.W,
})
// Set up the next series of shortcut keys.
i = -1
curFrame = ui.NewFrame("Frame")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
continue
}
// Cut a new frame every 2 items.
if i > 0 && i%2 == 0 {
curFrame = ui.NewFrame("Frame")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 2,
})
}
keyLabel := ui.NewLabel(ui.Label{
Text: row.Shortcut,
Font: balance.CodeLiteralFont,
})
keyLabel.Configure(ui.Config{
Background: render.RGBA(255, 255, 220, 255),
BorderSize: 1,
BorderStyle: ui.BorderSunken,
BorderColor: render.DarkGrey,
})
keyLabel.Resize(shortcutTabSize)
curFrame.Pack(keyLabel, ui.Pack{
Side: ui.W,
PadX: 1,
})
helpLabel := ui.NewLabel(ui.Label{
Text: row.Label,
Font: balance.UIFont,
})
helpLabel.Resize(infoTabSize)
curFrame.Pack(helpLabel, ui.Pack{
Side: ui.W,
PadX: 1,
})
}
return frame
}