doodle/pkg/gamepad/gamepad.go
Noah Petherbridge af6b8625d6 Flood Tool, Survival Mode for Azulian Tag
New features:
* Flood Tool for the editor. It replaces pixels of one color with another,
  contiguously. Has limits on how far from the original pixel it will color,
  to avoid infinite loops in case the user clicked on wide open void. The
  limit when clicking an existing color is 1200px or only a 600px limit if
  clicking into the void.
* Cheat code: 'master key' to play locked Story Mode levels.

Level GameRules feature added:
* A new tab in the Level Properties dialog
* Difficulty has been moved to this tab
* Survival Mode: for silver high score, longest time alive is better than
  fastest time, for Azulian Tag maps. Gold high score is still based on
  fastest time - find the hidden level exit without dying!

Tweaks to the Azulians' jump heights:
* Blue Azulian:  12 -> 14
* Red Azulian:   14 -> 18
* White Azulian: 16 -> 20

Bugs fixed:
* When editing your Palette to rename a color or add a new color, it wasn't
  possible to draw with that color until the editor was completely unloaded
  and reloaded; this is now fixed.
* Minor bugfix in Difficulty.String() for Peaceful (-1) difficulty to avoid
  a negative array index.
* Try and prevent user giving the same name to multiple swatches on their
  palette. Replacing the whole palette can let duplication through still.
2022-03-26 13:55:06 -07:00

396 lines
9.6 KiB
Go

/*
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 (%d) as Player 1", idx, ctrl.Name())
break
}
} 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 playerOne == nil || 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)
}