diff --git a/README.md b/README.md index 9cf2c52..9ff72ca 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,43 @@ Ctrl-Y Redo ``` +# Gamepad Controls + +The game supports Xbox and Nintendo style game controllers. The button +bindings are not yet customizable, except to choose between the +"X Style" or "N Style" for A/B and X/Y button mappings. + +Gamepad controls very depending on two modes the game can be in: + +## Mouse Mode + +The Gamepad emulates a mouse cursor in this mode. + +* The left analog stick moves a cursor around the screen. +* The right analog stick scrolls the level (title screen and editor) +* A or X button simulates a Left-click +* B or Y button simulates a Right-click +* L1 (left shoulder) emulates a Middle-click +* L2 (left trigger) closes the top-most window in the editor mode + (like the Backspace key). + +## Gameplay Mode + +When playing a level, the controls are as follows: + +* The left analog stick and the D-Pad will move the player character. +* A or X button to "Use" objects such as Warp Doors. +* B or Y button to "Jump" +* R1 (right shoulder) toggles between Mouse Mode and Gameplay Mode. + +You can use the R1 button to access Mouse Mode to interact with the +menus or click on the "Edit Level" button. + +Note: characters with antigravity (such as the Bird) can move in all +four directions but characters with gravity only move left and right +and have the dedicated "Jump" button. This differs from regular +keyboard controls where the Up arrow is to Jump. + # Built-In Doodads A brief introduction to the built-in doodads available so far: diff --git a/assets/sprites/pointer.png b/assets/sprites/pointer.png new file mode 100644 index 0000000..4656e23 Binary files /dev/null and b/assets/sprites/pointer.png differ diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 4baf60a..4bcbc41 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -16,6 +16,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/chatbot" + "git.kirsle.net/apps/doodle/pkg/gamepad" "git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/shmem" @@ -25,6 +26,7 @@ import ( "git.kirsle.net/go/render" "git.kirsle.net/go/render/sdl" "github.com/urfave/cli/v2" + sdl2 "github.com/veandco/go-sdl2/sdl" _ "image/png" ) @@ -70,6 +72,9 @@ func main() { usercfg.Current.CrosshairColor = balance.DefaultCrosshairColor } + // Set GameController style. + gamepad.SetStyle(gamepad.Style(usercfg.Current.ControllerStyle)) + app.Version = fmt.Sprintf("%s build %s%s. Built on %s", branding.Version, Build, @@ -149,6 +154,9 @@ func main() { balance.Height, ) + // Activate game controller event support. + sdl2.GameControllerEventState(1) + // Load the SDL fonts in from bindata storage. if fonts, err := assets.AssetDir("assets/fonts"); err == nil { for _, file := range fonts { diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 87bf946..4700bbc 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -92,6 +92,10 @@ var ( // Invulnerability time in seconds at respawn from checkpoint, in case // enemies are spawn camping. RespawnGodModeTimer = 3 * time.Second + + // GameController thresholds. + GameControllerMouseMoveMax float64 = 20 // Max pixels per tick to simulate mouse movement. + GameControllerScrollMin float64 = 0.3 // Minimum threshold for a right-stick scroll event. ) // Edit Mode Values diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 7e692b4..91b6bc7 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -13,6 +13,7 @@ var ( GoldCoin = "assets/sprites/gold.png" SilverCoin = "assets/sprites/silver.png" LockIcon = "assets/sprites/padlock.png" + CursorIcon = "assets/sprites/pointer.png" // Title Screen Font TitleScreenFont = render.Text{ diff --git a/pkg/doodle.go b/pkg/doodle.go index 82ceeeb..20b3f4c 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -10,6 +10,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/enum" + "git.kirsle.net/apps/doodle/pkg/gamepad" "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/levelpack" "git.kirsle.net/apps/doodle/pkg/log" @@ -131,7 +132,6 @@ func (d *Doodle) Run() error { // Poll for events. ev, err := d.Engine.Poll() - shmem.Cursor = render.NewPoint(ev.CursorX, ev.CursorY) if err != nil { log.Error("event poll error: %s", err) d.running = false @@ -139,6 +139,13 @@ func (d *Doodle) Run() error { } d.event = ev + // Let the gamepad controller check for events, if it's in MouseMode + // it will fake the mouse cursor. + gamepad.Loop(ev) + + // Globally store the cursor position. + shmem.Cursor = render.NewPoint(ev.CursorX, ev.CursorY) + // Command line shell. if d.shell.Open { } else if keybind.ShellKey(ev) { @@ -196,6 +203,9 @@ func (d *Doodle) Run() error { // Draw the debug overlay over all scenes. d.DrawDebugOverlay() + // Let the gamepad controller draw in case of MouseMode to show the cursor. + gamepad.Draw(d.Engine) + // Render the pixels to the screen. err = d.Engine.Present() if err != nil { @@ -246,6 +256,7 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window { CrosshairColor: &usercfg.Current.CrosshairColor, HideTouchHints: &usercfg.Current.HideTouchHints, DisableAutosave: &usercfg.Current.DisableAutosave, + ControllerStyle: &usercfg.Current.ControllerStyle, } return windows.MakeSettingsWindow(d.width, d.height, cfg) } diff --git a/pkg/gamepad/enum.go b/pkg/gamepad/enum.go new file mode 100644 index 0000000..4855b73 --- /dev/null +++ b/pkg/gamepad/enum.go @@ -0,0 +1,29 @@ +package gamepad + +// Enums and constants. + +// Mode of controller behavior (Player One) +type Mode int + +// Style of controller (Player One) +type Style int + +// Controller mode options. +const ( + // MouseMode: the joystick moves a mouse cursor around and + // the face buttons emulate mouse click events. + MouseMode Mode = iota + + // GameplayMode: to control the player character during Play Mode. + GameplayMode + + // EditorMode: to support the Level Editor. + EditorMode +) + +// Controller style options. +const ( + XStyle Style = iota // Xbox 360 layout (A button on bottom) + NStyle // Nintendo style (A button on right) + CustomStyle // Custom style (TODO) +) diff --git a/pkg/gamepad/gamepad.go b/pkg/gamepad/gamepad.go new file mode 100644 index 0000000..129a67f --- /dev/null +++ b/pkg/gamepad/gamepad.go @@ -0,0 +1,394 @@ +/* +Package gamepad provides game controller logic for the game. + +Controls + +The gamepad controls are currently hard-coded for Xbox 360 style controllers, and the +controller mappings vary depending on the "mode" of control. + +N Style and X Style + +If the gamepad control is set to "NStyle" then the A/B and X/Y buttons will be swapped +to match the labels of a Nintendo style controller. Since the game has relatively few +inputs needed now, this module maps them to a PrimaryButton and a SecondaryButton, +defined as: + +PrimaryButton: A or X button +SecondaryButton: B or Y button + +Mouse Mode + +- Left stick moves the mouse cursor (a cursor sprite is drawn on screen) +- Right stick scrolls the level (title screen or level editor) +- PrimaryButton emulates a left click. +- SecondaryButton emulates a right click. +- Left Shoulder emulates a middle click. +- Left Trigger (L2) closes the top-most window in the Editor (Backspace key) +- Right Shoulder toggles between Mouse Mode and other scene-specific mode. + +Gameplay Mode + +- Left stick moves the player character (left/right only). +- D-Pad also moves the player character (left/right only). +- PrimaryButton is to "Use" +- SecondaryButton is to "Jump" +- If the player has antigravity, up/down controls on left stick or D-Pad work too. +- Right Shoulder toggles between GameplayMode and MouseMode. +*/ +package gamepad + +import ( + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/apps/doodle/pkg/sprites" + "git.kirsle.net/go/render" + "git.kirsle.net/go/render/event" + "git.kirsle.net/go/ui" +) + +// Global state variables for gamepad support. +var ( + // PlayScene tells us whether antigravity is on, so directional controls work all directions. + PlayModeAntigravity bool + SceneName string // Set by doodle.Goto() so we know what scene name we're on. + + playerOne *int // controller index for Player 1 (main). + p1style Style + p1mode Mode + + // Mouse cursor + cursorVisible bool + cursorSprite *ui.Image + cursorLast render.Point // detect if mouse cursor took over from gamepad + + // MouseMode button state history to emulate mouse-ups. + leftClickLast bool + rightClickLast bool + middleClickLast bool + leftTriggerLast bool + + // MouseMode right stick last position, for arrow key (level scroll) emulation. + rightStickLast event.Vector + + // Gameplay mode last left-stick position. + leftStickLast event.Vector + dpLeftLast bool // D-Pad last positions. + dpRightLast bool + dpDownLast bool + dpUpLast bool + + // Right Shoulder last state (mode switch) + rightShoulderLast bool +) + +// SetControllerIndex sets which gamepad will be "Player One" +func SetControllerIndex(index int) { + playerOne = &index +} + +// UnsetController detaches the Player One controller. +func UnsetController() { + playerOne = nil +} + +// SetStyle sets the controller button style. +func SetStyle(s Style) { + p1style = s +} + +// SetMode sets the controller mode. +func SetMode(m Mode) { + p1mode = m +} + +// PrimaryButton returns whether the A or X button is pressed. +func PrimaryButton(ctrl event.GameController) bool { + if p1style == NStyle { + return ctrl.ButtonB() || ctrl.ButtonY() + } + return ctrl.ButtonA() || ctrl.ButtonX() +} + +// SecondaryButton returns whether the B or Y button is pressed. +func SecondaryButton(ctrl event.GameController) bool { + if p1style == NStyle { + return ctrl.ButtonA() || ctrl.ButtonX() + } + return ctrl.ButtonB() || ctrl.ButtonY() +} + +// Loop hooks the render events on each game tick. +func Loop(ev *event.State) { + // If we don't have a controller registered, watch out for one until we do. + if playerOne == nil { + if len(ev.Controllers) > 0 { + for idx, ctrl := range ev.Controllers { + SetControllerIndex(idx) + log.Info("gamepad: using controller #%d (%s) as Player 1", idx, ctrl) + } + } else { + return + } + } + + // Get our SDL2 controller. + ctrl, ok := ev.GetController(*playerOne) + if !ok { + log.Error("gamepad: controller #%d has gone away! Detaching as Player 1", playerOne) + playerOne = nil + return + } + + // Right Shoulder = toggle controller mode, handle this first. + if ctrl.ButtonR1() { + if !rightShoulderLast { + if SceneName == "Play" { + // Toggle between GameplayMode and MouseMode. + if p1mode == GameplayMode { + p1mode = MouseMode + } else { + p1mode = GameplayMode + } + + // Reset all button states. + ev.Left = false + ev.Right = false + ev.Up = false + ev.Down = false + ev.Enter = false + ev.Space = false + } + + rightShoulderLast = true + return + } + } else if rightShoulderLast { + rightShoulderLast = false + } + + // If we are in Play Mode, translate gamepad events into key events. + if p1mode == GameplayMode { + // D-Pad controls to move the player character. + if ctrl.ButtonLeft() { + ev.Left = true + dpLeftLast = true + } else if dpLeftLast { + ev.Left = false + dpLeftLast = false + } + + if ctrl.ButtonRight() { + ev.Right = true + dpRightLast = true + } else if dpRightLast { + ev.Right = false + dpRightLast = false + } + + // Antigravity on? Up/Down arrow emulation. + if PlayModeAntigravity { + if ctrl.ButtonUp() { + ev.Up = true + dpUpLast = true + } else if dpUpLast { + ev.Up = false + dpUpLast = false + } + + if ctrl.ButtonDown() { + ev.Down = true + dpDownLast = true + } else if dpDownLast { + ev.Down = false + dpDownLast = false + } + } + + // "Use" button. + if PrimaryButton(ctrl) { + ev.Space = true + ev.Enter = true // to click thru modals + leftClickLast = true + } else if leftClickLast { + ev.Space = false + ev.Enter = false + leftClickLast = false + } + + // Jump button. + if SecondaryButton(ctrl) { + ev.Up = true + rightClickLast = true + } else if rightClickLast { + ev.Up = false + rightClickLast = false + } + + // Left control stick to move the player character. + // TODO: analog movements. + leftStick := ctrl.LeftStick() + if leftStick.X != 0 { + if leftStick.X < -balance.GameControllerScrollMin { + ev.Left = true + ev.Right = false + } else if leftStick.X > balance.GameControllerScrollMin { + ev.Right = true + ev.Left = false + } else { + ev.Right = false + ev.Left = false + } + } else if leftStickLast.X != 0 { + ev.Left = false + ev.Right = false + } + + // Antigravity on? + if PlayModeAntigravity { + if leftStick.Y != 0 { + if leftStick.Y < -balance.GameControllerScrollMin { + ev.Up = true + ev.Down = false + } else if leftStick.Y > balance.GameControllerScrollMin { + ev.Down = true + ev.Up = false + } else { + ev.Down = false + ev.Up = false + } + } else if leftStickLast.Y != 0 { + ev.Up = false + ev.Down = false + } + } + + leftStickLast = leftStick + } + + // If we are emulating a mouse, handle that now. + if p1mode == MouseMode { + // Move the cursor. + leftStick := ctrl.LeftStick() + if leftStick.X != 0 || leftStick.Y != 0 { + cursorVisible = true + } else if cursorVisible { + // If the mouse cursor has moved behind our back (e.g., real mouse moved), turn off + // the MouseMode cursor sprite. + if cursorLast.X != ev.CursorX || cursorLast.Y != ev.CursorY { + cursorVisible = false + } + } + + ev.CursorX += int(leftStick.X * balance.GameControllerMouseMoveMax) + ev.CursorY += int(leftStick.Y * balance.GameControllerMouseMoveMax) + + // Constrain the cursor inside window boundaries. + w, h := shmem.CurrentRenderEngine.WindowSize() + if ev.CursorX < 0 { + ev.CursorX = 0 + } else if ev.CursorX > w { + ev.CursorX = w + } + if ev.CursorY < 0 { + ev.CursorY = 0 + } else if ev.CursorY > h { + ev.CursorY = h + } + + // Store last cursor point so we can detect mouse movement outside the gamepad. + cursorLast = render.NewPoint(ev.CursorX, ev.CursorY) + + // Are we clicking? + if PrimaryButton(ctrl) { + ev.Button1 = true // left-click + leftClickLast = true + } else if leftClickLast { + ev.Button1 = false + leftClickLast = false + } + + // Right-click + if SecondaryButton(ctrl) { + ev.Button3 = true // right-click + rightClickLast = true + } else if rightClickLast { + ev.Button3 = false + rightClickLast = false + } + + // Middle-click + if ctrl.ButtonL1() { + ev.Button2 = true // middle click + middleClickLast = true + } else if middleClickLast { + ev.Button2 = false + middleClickLast = false + } + + // Left Trigger = Backspace (close active window) + if ctrl.ButtonL2() { + if !leftTriggerLast { + ev.SetKeyDown(`\b`, true) + } + leftTriggerLast = true + } else if leftTriggerLast { + ev.SetKeyDown(`\b`, false) + leftTriggerLast = false + } + + // Arrow Key emulation on the right control stick, e.g. for Level Editor. + rightStick := ctrl.RightStick() + if rightStick.X != 0 { + if rightStick.X < -balance.GameControllerScrollMin { + ev.Left = true + ev.Right = false + } else if rightStick.X > balance.GameControllerScrollMin { + ev.Right = true + ev.Left = false + } else { + ev.Right = false + ev.Left = false + } + } else if rightStickLast.X != 0 { + ev.Left = false + ev.Right = false + } + + if rightStick.Y != 0 { + if rightStick.Y < -balance.GameControllerScrollMin { + ev.Up = true + ev.Down = false + } else if rightStick.Y > balance.GameControllerScrollMin { + ev.Down = true + ev.Up = false + } else { + ev.Down = false + ev.Up = false + } + } else if rightStickLast.Y != 0 { + ev.Up = false + ev.Down = false + } + + rightStickLast = rightStick + } +} + +// Draw the cursor on screen if the game controller is emulating a mouse. +func Draw(e render.Engine) { + if p1mode != MouseMode || !cursorVisible { + return + } + + if cursorSprite == nil { + img, err := sprites.LoadImage(e, balance.CursorIcon) + if err != nil { + log.Error("gamepad: couldn't load cursor sprite (%s): %s", balance.CursorIcon, err) + return + } + cursorSprite = img + } + + cursorSprite.Present(e, shmem.Cursor) +} diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 0c5405c..b75b072 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/gamepad" "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/levelpack" @@ -282,6 +283,9 @@ func (s *PlayScene) setupAsync(d *Doodle) error { // runtime, + the bitmap generation is pretty wicked fast anyway. loadscreen.PreloadAllChunkBitmaps(s.Level.Chunker) + // Gamepad: put into GameplayMode. + gamepad.SetMode(gamepad.GameplayMode) + s.startTime = time.Now() s.perfectRun = true s.running = true @@ -416,6 +420,7 @@ func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, cen // EditLevel toggles out of Play Mode to edit the level. func (s *PlayScene) EditLevel() { log.Info("Edit Mode, Go!") + gamepad.SetMode(gamepad.MouseMode) s.d.Goto(&EditorScene{ Filename: s.Filename, Level: s.Level, @@ -518,6 +523,7 @@ func (s *PlayScene) ShowEndLevelModal(success bool, title, message string) { OnRestartLevel: s.RestartLevel, OnRetryCheckpoint: s.RetryCheckpoint, OnExitToMenu: func() { + gamepad.SetMode(gamepad.MouseMode) s.d.Goto(&MainScene{}) }, } @@ -589,6 +595,9 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error { return nil } + // Inform the gamepad controller whether we have antigravity controls. + gamepad.PlayModeAntigravity = s.antigravity || !s.Player.HasGravity() + // Update debug overlay values. *s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).String() *s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String() @@ -614,6 +623,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error { // Switching to Edit Mode? if s.CanEdit && keybind.GotoEdit(ev) { + gamepad.SetMode(gamepad.MouseMode) s.EditLevel() return nil } diff --git a/pkg/scene.go b/pkg/scene.go index 45cb452..9f1eb33 100644 --- a/pkg/scene.go +++ b/pkg/scene.go @@ -1,6 +1,7 @@ package doodle import ( + "git.kirsle.net/apps/doodle/pkg/gamepad" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/go/render/event" ) @@ -23,6 +24,9 @@ type Scene interface { // Goto a scene. First it unloads the current scene. func (d *Doodle) Goto(scene Scene) error { + // Inform the gamepad controller what scene. + gamepad.SceneName = scene.Name() + // Clear any debug labels. customDebugLabels = []debugLabel{} diff --git a/pkg/uix/magic-form/magic_form.go b/pkg/uix/magic-form/magic_form.go index eedc08e..a652f29 100644 --- a/pkg/uix/magic-form/magic_form.go +++ b/pkg/uix/magic-form/magic_form.go @@ -59,9 +59,10 @@ type Field struct { Frame *ui.Frame // Variable bindings, the type may infer to be: - BoolVariable *bool // Checkbox - TextVariable *string // Textbox - Options []Option // Selectbox + BoolVariable *bool // Checkbox + TextVariable *string // Textbox + Options []Option // Selectbox + SelectValue interface{} // Selectbox default choice // Tooltip to add to a form control. // Checkbox only for now. @@ -208,8 +209,9 @@ func (form Form) Create(into *ui.Frame, fields []Field) { Font: row.Font, }) frame.Pack(btn, ui.Pack{ - Side: ui.W, - FillX: true, + Side: ui.W, + FillX: true, + Expand: true, }) if row.Options != nil { @@ -218,7 +220,11 @@ func (form Form) Create(into *ui.Frame, fields []Field) { } } - btn.Handle(ui.Click, func(ed ui.EventData) error { + if row.SelectValue != nil { + btn.SetValue(row.SelectValue) + } + + btn.Handle(ui.Change, func(ed ui.EventData) error { if selection, ok := btn.GetValue(); ok { if row.OnSelect != nil { row.OnSelect(selection.Value) @@ -227,6 +233,7 @@ func (form Form) Create(into *ui.Frame, fields []Field) { return nil }) + btn.Supervise(form.Supervisor) form.Supervisor.Add(btn) } } diff --git a/pkg/usercfg/usercfg.go b/pkg/usercfg/usercfg.go index 3139be6..8ef3701 100644 --- a/pkg/usercfg/usercfg.go +++ b/pkg/usercfg/usercfg.go @@ -37,6 +37,7 @@ type Settings struct { CrosshairColor render.Color HideTouchHints bool `json:",omitempty"` DisableAutosave bool `json:",omitempty"` + ControllerStyle int // Secret boolprops from balance/boolprops.go ShowHiddenDoodads bool `json:",omitempty"` diff --git a/pkg/windows/settings.go b/pkg/windows/settings.go index 0a70c20..ee7339d 100644 --- a/pkg/windows/settings.go +++ b/pkg/windows/settings.go @@ -5,9 +5,11 @@ import ( "strings" "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/gamepad" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/shmem" + magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form" "git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/go/render" @@ -30,6 +32,7 @@ type Settings struct { CrosshairColor *render.Color HideTouchHints *bool DisableAutosave *bool + ControllerStyle *int // Configuration options. SceneName string // name of scene which called this window @@ -81,6 +84,7 @@ func NewSettingsWindow(cfg Settings) *ui.Window { // 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) @@ -721,3 +725,82 @@ func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int) 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: "About", + Font: balance.LabelFont, + }, + { + Label: "Play Sketchy Maze with an Xbox or Nintendo controller!\n\n" + + "Full customization options aren't here yet, but you can\n" + + "choose between the 'X Style' or 'N Style' profile below.\n" + + "'N Style' will swap 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)) + saveGameSettings() + }, + }, + { + Label: "\nThe 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 +}