From 864156da532df9d992041c0160234e709d2a0719 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 19 Jun 2021 22:14:41 -0700 Subject: [PATCH] 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. --- cmd/doodad/main.go | 5 +- cmd/doodle/main.go | 10 +- pkg/balance/boolprops.go | 73 ++--- pkg/balance/theme.go | 8 + pkg/commands.go | 9 +- pkg/doodads/json.go | 4 +- pkg/doodle.go | 24 +- pkg/editor_scene.go | 5 +- pkg/editor_ui.go | 8 +- pkg/editor_ui_menubar.go | 7 + pkg/editor_ui_palette.go | 3 +- pkg/editor_ui_toolbar.go | 3 +- pkg/keybind/keybind.go | 74 +++++ pkg/level/fmt_json.go | 4 +- pkg/main_scene.go | 23 +- pkg/play_scene.go | 2 +- pkg/scripting/events.go | 4 +- pkg/usercfg/usercfg.go | 97 +++++++ pkg/windows/doodad_dropper.go | 3 +- pkg/windows/palette_editor.go | 3 +- pkg/windows/settings.go | 513 ++++++++++++++++++++++++++++++---- 21 files changed, 762 insertions(+), 120 deletions(-) create mode 100644 pkg/usercfg/usercfg.go diff --git a/cmd/doodad/main.go b/cmd/doodad/main.go index 91e9c39..205f007 100644 --- a/cmd/doodad/main.go +++ b/cmd/doodad/main.go @@ -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) } } diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 5bd8f88..b342cb6 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -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 diff --git a/pkg/balance/boolprops.go b/pkg/balance/boolprops.go index 44749f1..e1b8cf3 100644 --- a/pkg/balance/boolprops.go +++ b/pkg/balance/boolprops.go @@ -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") diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index f157a87..a167a25 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -77,6 +77,14 @@ var ( Color: render.Black, } + // CodeLiteralFont for rendering -like text. + CodeLiteralFont = render.Text{ + Size: 11, + PadX: 3, + FontFilename: "DejaVuSansMono.ttf", + Color: render.Magenta, + } + // Small font SmallFont = render.Text{ Size: 10, diff --git a/pkg/commands.go b/pkg/commands.go index 3468553..da0470a 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -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 ") 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 ") diff --git a/pkg/doodads/json.go b/pkg/doodads/json.go index b828e27..85e086e 100644 --- a/pkg/doodads/json.go +++ b/pkg/doodads/json.go @@ -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) diff --git a/pkg/doodle.go b/pkg/doodle.go index 0c14a18..630180f 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -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() { diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index ecd4340..72eef8e 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -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.") diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 9f45ac6..0016810 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -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, diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index b6ea919..595b164 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -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 diff --git a/pkg/editor_ui_palette.go b/pkg/editor_ui_palette.go index 9913b72..ab8a076 100644 --- a/pkg/editor_ui_palette.go +++ b/pkg/editor_ui_palette.go @@ -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, diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index e22e9a4..cb47054 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -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 diff --git a/pkg/keybind/keybind.go b/pkg/keybind/keybind.go index 682a463..211e96f 100644 --- a/pkg/keybind/keybind.go +++ b/pkg/keybind/keybind.go @@ -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 diff --git a/pkg/level/fmt_json.go b/pkg/level/fmt_json.go index 450afed..70bf770 100644 --- a/pkg/level/fmt_json.go +++ b/pkg/level/fmt_json.go @@ -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) diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 5cd1d9e..584aee9 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -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), diff --git a/pkg/play_scene.go b/pkg/play_scene.go index c42d724..b3f0a52 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -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. diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go index d132add..0c426d8 100644 --- a/pkg/scripting/events.go +++ b/pkg/scripting/events.go @@ -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) } diff --git a/pkg/usercfg/usercfg.go b/pkg/usercfg/usercfg.go new file mode 100644 index 0000000..cccd3ef --- /dev/null +++ b/pkg/usercfg/usercfg.go @@ -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 +} diff --git a/pkg/windows/doodad_dropper.go b/pkg/windows/doodad_dropper.go index 5ef5fc8..26553e4 100644 --- a/pkg/windows/doodad_dropper.go +++ b/pkg/windows/doodad_dropper.go @@ -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 } diff --git a/pkg/windows/palette_editor.go b/pkg/windows/palette_editor.go index 5d0c8dc..99d846e 100644 --- a/pkg/windows/palette_editor.go +++ b/pkg/windows/palette_editor.go @@ -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 } diff --git a/pkg/windows/settings.go b/pkg/windows/settings.go index 9fe4930..7e24fbd 100644 --- a/pkg/windows/settings.go +++ b/pkg/windows/settings.go @@ -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 +}