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