package windows import ( "strings" "git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/gamepad" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" magicform "git.kirsle.net/SketchyMaze/doodle/pkg/uix/magic-form" "git.kirsle.net/SketchyMaze/doodle/pkg/usercfg" "git.kirsle.net/SketchyMaze/doodle/pkg/userdir" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" ) // Settings window. type Settings struct { // Settings passed in by doodle Supervisor *ui.Supervisor Engine render.Engine // Boolean bindings. DebugOverlay *bool DebugCollision *bool HorizontalToolbars *bool EnableFeatures *bool CrosshairSize *int CrosshairColor *render.Color HideTouchHints *bool DisableAutosave *bool ControllerStyle *int // Configuration options. SceneName string // name of scene which called this window ActiveTab string // specify the tab to open OnApply func() OnOpenCheatsWindow func() *ui.Window // user opens the Cheats Menu // The Settings window owns the Cheats window to ensure only one opens at a time. cheatsWindow *ui.Window } // 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 { var ( Width = 400 Height = 360 ) window := ui.NewWindow("Settings") window.SetButtons(ui.CloseButton) window.Configure(ui.Config{ Width: Width, Height: Height, Background: render.Grey, }) /////////// // Tab Bar tabFrame := ui.NewTabFrame("Tab Frame") tabFrame.SetBackground(render.DarkGrey) window.Pack(tabFrame, ui.Pack{ Side: ui.N, FillX: true, }) // Make the tabs cfg.makeOptionsTab(tabFrame, Width, Height) cfg.makeControlsTab(tabFrame, Width, Height) cfg.makeControllerTab(tabFrame, Width, Height) cfg.makeExperimentalTab(tabFrame, Width, Height) tabFrame.Supervise(cfg.Supervisor) 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(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { tab := tabFrame.AddTab("Options", ui.NewLabel(ui.Label{ Text: "Options", Font: balance.TabFont, })) tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) // Common click handler for all settings, // so we can write the updated info to disk. onClick := func() { saveGameSettings() } // The CrosshairSize is ideally a 0-100 (percent) how big the editor // crosshair is, but options now are only 0% or 100% so it presents // this as a checkbox for now. var crosshairEnabled = *c.CrosshairSize > 0 form := magicform.Form{ Supervisor: c.Supervisor, Engine: c.Engine, Vertical: true, LabelWidth: 150, } form.Create(tab, []magicform.Field{ { Label: "Game Options", Font: balance.LabelFont, }, { Label: "Hide touchscreen control hints during Play Mode", Font: balance.UIFont, BoolVariable: c.HideTouchHints, OnClick: onClick, }, { Label: "Level & Doodad Editor", Font: balance.LabelFont, }, { Label: "Horizontal instead of vertical toolbars", Font: balance.UIFont, BoolVariable: c.HorizontalToolbars, OnClick: onClick, Tooltip: ui.Tooltip{ Text: "Note: reload your level after changing this option.\n" + "Playtesting and returning will do.", Edge: ui.Top, }, }, { Label: "Disable auto-save in the Editor", Font: balance.UIFont, BoolVariable: c.DisableAutosave, OnClick: onClick, }, { Label: "Draw a crosshair at the mouse cursor.", Font: balance.UIFont, BoolVariable: &crosshairEnabled, OnClick: func() { if crosshairEnabled { *c.CrosshairSize = 100 } else { *c.CrosshairSize = 0 } onClick() }, }, { Type: magicform.Color, Label: "Crosshair color:", Font: balance.UIFont, Color: c.CrosshairColor, OnClick: func() { onClick() }, }, { Label: "My Custom Content", Font: balance.LabelFont, }, { Label: "Levels and doodads you create in-game are placed in your\n" + "Profile Directory, which you can access below:", Font: balance.UIFont, }, { Buttons: []magicform.Field{ { Label: "Open profile directory", Font: balance.UIFont, ButtonStyle: &balance.ButtonPrimary, OnClick: func() { path := strings.ReplaceAll(userdir.ProfileDirectory, "\\", "/") if path[0] != '/' { path = "/" + path } native.OpenURL("file://" + path) }, }, }, }, }) return tab } // Settings Window "Controls" Tab func (c Settings) makeControlsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { frame := tabFrame.AddTab("Controls", ui.NewLabel(ui.Label{ Text: "Controls", Font: balance.TabFont, })) frame.Resize(render.NewRect(Width-4, Height-frame.Size().H-46)) 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) ) 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: "q", 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: 1, }) } 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 } // Settings Window "Experimental" Tab func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { tab := tabFrame.AddTab("Experimental", ui.NewLabel(ui.Label{ Text: "Experimental", Font: balance.TabFont, })) tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) // Common click handler for all settings, // so we can write the updated info to disk. onClick := func() { saveGameSettings() } form := magicform.Form{ Supervisor: c.Supervisor, Engine: c.Engine, Vertical: true, LabelWidth: 150, } form.Create(tab, []magicform.Field{ { Label: "Enable Experimental Features", Font: balance.LabelFont, }, { Label: "The setting below can enable experimental features in this\n" + "game. These are features which are still in development and\n" + "may have unstable or buggy behavior.", Font: balance.UIFont, }, { Label: "Viewport window", Font: balance.LabelFont, }, { Label: "This option in the Level menu opens another view into\n" + "the level. Has glitchy wallpaper problems.", Font: balance.UIFont, }, { BoolVariable: c.EnableFeatures, Label: "Enable experimental features", Font: balance.UIFont, OnClick: onClick, }, { Label: "Restart the game for changes to take effect.", Font: balance.UIFont, }, { Label: "Cheat Codes", Font: balance.LabelFont, }, { Label: "The ` (or ~) key will open a developer console into which you\n" + "can enter cheat codes (see your guidebook). For touch-only\n" + "devices, most of the useful cheats may be toggled via the\n" + "Cheats Menu which you can launch by clicking below:", Font: balance.UIFont, }, { Buttons: []magicform.Field{ { ButtonStyle: &balance.ButtonPrimary, Label: "Open Cheats Window", Font: balance.UIFont.Update(render.Text{ PadY: 0, }), OnClick: func() { if c.OnOpenCheatsWindow != nil { if c.cheatsWindow != nil { c.cheatsWindow.Hide() c.cheatsWindow.Destroy() c.cheatsWindow = nil } c.cheatsWindow = c.OnOpenCheatsWindow() c.cheatsWindow.Show() } else { shmem.FlashError("OnOpenCheatsWindow not handled") } }, }, }, }, }) return tab } // Settings Window "Controller" Tab func (c Settings) makeControllerTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { tab := tabFrame.AddTab("Gamepad", ui.NewLabel(ui.Label{ Text: "Gamepad", Font: balance.TabFont, })) tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) // Render the form. form := magicform.Form{ Supervisor: c.Supervisor, Engine: c.Engine, Vertical: true, LabelWidth: 150, } form.Create(tab, []magicform.Field{ { Label: "Play with an Xbox or Nintendo controller!", Font: balance.LabelFont, }, { Label: "If you have a Nintendo-style controller (your A button is on\n" + "the right and B button on bottom), pick 'N Style' to reverse\n" + "the A/B and X/Y buttons.", Font: balance.UIFont, }, { Label: "Button Style:", Font: balance.LabelFont, Type: magicform.Selectbox, Options: []magicform.Option{ { Label: "X Style (default)", Value: int(gamepad.XStyle), }, { Label: "N Style", Value: int(gamepad.NStyle), }, }, SelectValue: &c.ControllerStyle, OnSelect: func(v interface{}) { style, _ := v.(int) log.Error("style: %d", style) gamepad.SetStyle(gamepad.Style(style)) *c.ControllerStyle = style saveGameSettings() }, }, { Label: "The gamepad controls vary between two modes:", Font: balance.UIFont, }, { Label: "Mouse Mode (outside of gameplay)", Font: balance.LabelFont, }, { Label: "The left analog stick moves a mouse cursor around.\n" + "The right analog stick scrolls the level around.\n" + "A or X: Left-click B or Y: Right-click\n" + "L1: Middle-click L2: Close window", Font: balance.UIFont, }, { Label: "Gameplay Mode", Font: balance.LabelFont, }, { Label: "Left stick or D-Pad to move the player around.\n" + "A or X: 'Use' B or Y: 'Jump'\n" + "R1: Toggle between Mouse and Gameplay controls.", Font: balance.UIFont, }, }) return tab }