From 4de0126b19c7341e552d3900a9c30aa55afd085b Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 19 Feb 2022 18:25:36 -0800 Subject: [PATCH] Game Controller Support Adds support for Xbox and Nintendo style game controllers. The gamepad controls are documented on the README and in the game's Settings window. The buttons are not customizable yet, except that the player can choose between two button styles: * X Style (default): "A" button is on the bottom and "B" on the right. * N Style: swaps the A/B and the X/Y buttons to use a Nintendo-style layout instead of an Xbox-style. --- README.md | 37 +++ assets/sprites/pointer.png | Bin 0 -> 729 bytes cmd/doodle/main.go | 8 + pkg/balance/numbers.go | 4 + pkg/balance/theme.go | 1 + pkg/doodle.go | 13 +- pkg/gamepad/enum.go | 29 +++ pkg/gamepad/gamepad.go | 394 +++++++++++++++++++++++++++++++ pkg/play_scene.go | 10 + pkg/scene.go | 4 + pkg/uix/magic-form/magic_form.go | 19 +- pkg/usercfg/usercfg.go | 1 + pkg/windows/settings.go | 83 +++++++ 13 files changed, 596 insertions(+), 7 deletions(-) create mode 100644 assets/sprites/pointer.png create mode 100644 pkg/gamepad/enum.go create mode 100644 pkg/gamepad/gamepad.go 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 0000000000000000000000000000000000000000..4656e23cb6d65a355e25d131ec2863b0a731a326 GIT binary patch literal 729 zcmV;~0w(>5P)EX>4Tx04R}tkv&MmKp2MKrb+0Yt2!cN#j!sUBE>hxmNufoI2Y2`I-n}pPeFq4Q3e&8v2|&|r zGo4I`+1#oadWC=pKDxYTjZ%KqQ`HhG`RT5YKGd z2Iqa^2rJ1d@j3ChNe?7`zBx-kgE(v zjs;YqL3aJ%fAG7vR$*$=OA04|?ia`T7y&}NK(p>R-^Y&AJOP5wz?I(iR~x|WC+YRJ z7Ci#`w}Ff6wx;X>mpj1VlOdb3D+Or^g#z$?M&FbJ25y1gHLq{2bDTZ^8R}K)1~@nb zMvIia?o)S9XW#z4)9T+35q5HbILfb500006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru<^mJ|BqU^+#NYq`02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{006N`L_t(Y$Gz0u5rZHQ1mN8z9i;(NiLGfT7N8};j^uN#=(bi&1M0nUa5S=yL zMO4ys2T^I`RYWz-R}j^ns1|wc6+#H_qm3lZ%y9r8JW?lHE9tU9;CN11{kia0_1nKH)#WFB|||!=D&mQUh`-FkXpZ00000 LNkvXXu0mjfn$b41 literal 0 HcmV?d00001 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 +}