Cheats Menu UI

* Added a Cheats Menu UI accessible from the Settings window's "Experimental"
  tab and from there you can enable the Cheats Menu from the "Help" screen of
  the gameplay mode.
* Commonly used cheats all have corresponding buttons to click on, especially
  helpful for touchscreen devices like the Pinephone where keyboard input
  doesn't always work reliably.
* The buttons in the Cheats Menu just automate entry of the cheat commands.
* `boolProp` command has a new `flip` option to toggle their value (e.g.
  `boolProp show-hidden-doodads flip`)
This commit is contained in:
Noah 2023-01-02 12:36:12 -08:00
parent 06dd30893c
commit a10a09a818
15 changed files with 580 additions and 23 deletions

View File

@ -1,5 +1,17 @@
# Changes
## v0.13.2 (TBD)
* In the level editor, you can now use the Pan Tool to access the actor
properties of doodads you've dropped into your level. Similar to the
Actor Tool, when you mouse-over an actor on your level it will highlight
in a grey box and a gear icon in the corner can be clicked to access
its properties. Making the properties available for the Pan Tool can
help with touchscreen devices, where it is difficult to touch the
properties button without accidentally dragging the actor elsewhere
on your level as might happen with the Actor Tool!
* Start distributing AppImage releases for GNU/Linux (64-bit and 32-bit)
## v0.13.1 (Oct 10 2022)
This release brings a handful of minor new features to the game.

View File

@ -1,5 +1,7 @@
package balance
import magicform "git.kirsle.net/SketchyMaze/doodle/pkg/uix/magic-form"
// Store a copy of the PlayerCharacterDoodad original value.
var playerCharacterDefault string
@ -50,3 +52,39 @@ var CheatActors = map[string]string{
"megaton weight": "anvil",
"play as thief": "thief",
}
// Options for the "Play as:" drop-down in the Cheat Menu window.
var CheatMenuActors = []magicform.Option{
{
Value: "",
Label: "Play as . . .",
},
{
Value: "boy.doodad",
Label: "Boy",
},
{
Value: "thief.doodad",
Label: "Thief",
},
{
Value: "azu-blu.doodad",
Label: "Azulian",
},
{
Value: "bird-red.doodad",
Label: "Bird",
},
{
Value: "crusher.doodad",
Label: "Crusher",
},
{
Value: "snake.doodad",
Label: "Snake",
},
{
Value: "anvil.doodad",
Label: "Anvil",
},
}

View File

@ -227,6 +227,9 @@ var (
DoodadDropperCols = 6 // rows/columns of buttons
DoodadDropperRows = 3
// CheatsMenu window settings.
CheatsMenuBackground = render.RGBA(0, 153, 153, 255)
// Button styles, customized in init().
ButtonPrimary = style.DefaultButton
ButtonDanger = style.DefaultButton

View File

@ -9,7 +9,7 @@ import (
const (
AppName = "Sketchy Maze"
Summary = "A drawing-based maze game"
Version = "0.13.1"
Version = "0.13.2"
Website = "https://www.sketchymaze.com"
Copyright = "2022 Noah Petherbridge"
Byline = "a game by Noah Petherbridge."

View File

@ -7,11 +7,48 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"git.kirsle.net/SketchyMaze/doodle/pkg/windows"
"git.kirsle.net/go/ui"
)
// IsDefaultPlayerCharacter checks whether the DefaultPlayerCharacter doodad has
// been modified
// MakeCheatsWindow initializes the windows/cheats_menu.go window from anywhere you need it,
// binding all the variables in. If you pass a nil Supervisor, this function will attempt to
// find one based on your Scene and
func (d *Doodle) MakeCheatsWindow(supervisor *ui.Supervisor) *ui.Window {
// If not given a supervisor, try and find one.
if supervisor == nil {
if v, err := d.FindLikelySupervisor(); err != nil {
d.FlashError("Couldn't make cheats window: %s", err)
return nil
} else {
supervisor = v
}
}
cfg := windows.CheatsMenu{
Supervisor: supervisor,
Engine: d.Engine,
SceneName: func() string {
return d.Scene.Name()
},
RunCommand: func(command string) {
d.shell.Execute(command)
},
OnSetPlayerCharacter: func(doodad string) {
if scene, ok := d.Scene.(*PlayScene); ok {
scene.SetPlayerCharacter(doodad)
} else {
shmem.FlashError("This only works during Play Mode.")
}
},
}
return windows.MakeCheatsMenu(cfg)
}
// SetPlayerCharacter -- this is designed to be called in-game with the developer
// console. Sets your player character to whatever doodad you want, not just the
// few that have cheat codes. If you set an invalid filename, you become the

View File

@ -315,13 +315,14 @@ func (c Command) BoolProp(d *Doodle) error {
}
if len(c.Args) != 2 {
return errors.New("Usage: boolProp <name> [true or false]")
return errors.New("Usage: boolProp <name> [true, false, flip]")
}
var (
name = c.Args[0]
value = c.Args[1]
truthy = value[0] == 't' || value[0] == 'T' || value[0] == '1'
flip = value == "flip"
ok = true
)
@ -331,16 +332,28 @@ func (c Command) BoolProp(d *Doodle) error {
d.Debug = truthy
case "DebugOverlay":
case "DO":
DebugOverlay = truthy
if flip {
DebugOverlay = !DebugOverlay
} else {
DebugOverlay = truthy
}
case "DebugCollision":
case "DC":
DebugCollision = truthy
if flip {
DebugCollision = !DebugCollision
} else {
DebugCollision = truthy
}
default:
ok = false
}
if ok {
d.Flash("Set boolProp %s=%s", name, strconv.FormatBool(truthy))
if flip {
d.Flash("Toggled boolProp %s", name)
} else {
d.Flash("Set boolProp %s=%s", name, strconv.FormatBool(truthy))
}
} else {
// Try the global boolProps in balance package.
if err := balance.BoolProp(name, truthy); err != nil {

View File

@ -255,6 +255,9 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window {
OnApply: func() {
},
OnOpenCheatsWindow: func() *ui.Window {
return d.MakeCheatsWindow(supervisor)
},
// Boolean checkbox bindings
DebugOverlay: &DebugOverlay,

View File

@ -54,7 +54,7 @@ type PlayScene struct {
cheated bool // user has entered a cheat code while playing
// UI widgets.
supervisor *ui.Supervisor
Supervisor *ui.Supervisor
screen *ui.Frame // A window sized invisible frame to position UI elements.
menubar *ui.MenuBar
editButton *ui.Button
@ -87,6 +87,9 @@ type PlayScene struct {
invenItems []string // item list
invenDoodads map[string]*uix.Canvas
// Cheats window
cheatsWindow *ui.Window
// Elapsed Time frame.
timerFrame *ui.Frame
timerPerfectImage *ui.Image
@ -109,7 +112,7 @@ func (s *PlayScene) Name() string {
func (s *PlayScene) Setup(d *Doodle) error {
s.d = d
s.scripting = scripting.NewSupervisor()
s.supervisor = ui.NewSupervisor()
s.Supervisor = ui.NewSupervisor()
// Show the loading screen.
loadscreen.ShowWithProgress()
@ -167,7 +170,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
s.EditLevel()
return nil
})
s.supervisor.Add(s.editButton)
s.Supervisor.Add(s.editButton)
// Set up the inventory HUD.
s.setupInventoryHud()
@ -751,7 +754,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
// Update the timer.
s.timerLabel.Text = savegame.FormatDuration(time.Since(s.startTime))
s.supervisor.Loop(ev)
s.Supervisor.Loop(ev)
// Has the window been resized?
if ev.WindowResized || s.drawing.Point().IsZero() {
@ -845,7 +848,7 @@ func (s *PlayScene) Draw(d *Doodle) error {
s.DrawTouchable()
// Let Supervisor draw menus
s.supervisor.Present(d.Engine)
s.Supervisor.Present(d.Engine)
return nil
}

View File

@ -20,7 +20,7 @@ func (u *PlayScene) setupMenuBar(d *Doodle) *ui.MenuBar {
// TODO: de-duplicate code from MainScene
if u.winLevelPacks == nil {
u.winLevelPacks = windows.NewLevelPackWindow(windows.LevelPack{
Supervisor: u.supervisor,
Supervisor: u.Supervisor,
Engine: d.Engine,
OnPlayLevel: func(lp levelpack.LevelPack, which levelpack.Level) {
@ -66,7 +66,7 @@ func (u *PlayScene) setupMenuBar(d *Doodle) *ui.MenuBar {
levelMenu.AddSeparator()
levelMenu.AddItemAccel("New viewport", "v", func() {
pip := windows.MakePiPWindow(d.width, d.height, windows.PiP{
Supervisor: u.supervisor,
Supervisor: u.Supervisor,
Engine: u.d.Engine,
Level: u.Level,
Event: u.d.event,
@ -76,9 +76,22 @@ func (u *PlayScene) setupMenuBar(d *Doodle) *ui.MenuBar {
})
}
d.MakeHelpMenu(menu, u.supervisor)
helpMenu := d.MakeHelpMenu(menu, u.Supervisor)
if usercfg.Current.EnableCheatsMenu {
helpMenu.AddSeparator()
helpMenu.AddItem("Cheats Menu", func() {
if u.cheatsWindow != nil {
u.cheatsWindow.Hide()
u.cheatsWindow.Destroy()
u.cheatsWindow = nil
}
menu.Supervise(u.supervisor)
u.cheatsWindow = u.d.MakeCheatsWindow(u.Supervisor)
u.cheatsWindow.Show()
})
}
menu.Supervise(u.Supervisor)
menu.Compute(d.Engine)
return menu

View File

@ -30,8 +30,8 @@ func (s *PlayScene) LoopTouchable(ev *event.State) {
// Don't do any of this if the mouse is over the menu bar, so
// clicking on the menus doesn't make the character move or jump.
if cursor.Inside(s.menubar.Rect()) || s.supervisor.GetModal() != nil ||
s.supervisor.IsPointInWindow(cursor) {
if cursor.Inside(s.menubar.Rect()) || s.Supervisor.GetModal() != nil ||
s.Supervisor.IsPointInWindow(cursor) {
return
}

View File

@ -2,6 +2,7 @@ package doodle
import (
"bytes"
"errors"
"fmt"
"strings"
@ -44,6 +45,20 @@ func (d *Doodle) PromptPre(question string, prefilled string, callback func(stri
d.shell.Open = true
}
// FindLikelySupervisor will locate a most likely ui.Supervisor depending on the current Scene,
// if it understands the Scene and knows where it keeps its Supervisor.
func (d *Doodle) FindLikelySupervisor() (*ui.Supervisor, error) {
switch scene := d.Scene.(type) {
case *EditorScene:
return scene.UI.Supervisor, nil
case *PlayScene:
return scene.Supervisor, nil
case *MainScene:
return scene.Supervisor, nil
}
return nil, errors.New("couldn't find a Supervisor")
}
// Shell implements the developer console in-game.
type Shell struct {
parent *Doodle

View File

@ -152,9 +152,15 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
btn.Compute(form.Engine)
form.Supervisor.Add(btn)
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
frame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 4,
PadX: 2,
PadY: 2,
})
}

View File

@ -3,9 +3,9 @@ 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.
- 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
@ -45,6 +45,7 @@ type Settings struct {
ShowHiddenDoodads bool `json:",omitempty"`
WriteLockOverride bool `json:",omitempty"`
JSONIndent bool `json:",omitempty"`
EnableCheatsMenu bool `json:",omitempty"`
// Bookkeeping.
UpdatedAt time.Time

372
pkg/windows/cheats_menu.go Normal file
View File

@ -0,0 +1,372 @@
package windows
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"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/go/render"
"git.kirsle.net/go/ui"
)
// CheatsMenu window.
type CheatsMenu struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
// SceneName: the caller will provide a fresh SceneName since
// the cheats window could span multiple scenes.
SceneName func() string
// Window wants to run a developer shell command (e.g. cheat codes).
RunCommand func(string)
OnSetPlayerCharacter func(string)
}
// MakeCheatsMenu initializes a settings window for any scene.
// The window width/height are the actual SDL2 window dimensions.
func MakeCheatsMenu(cfg CheatsMenu) *ui.Window {
var (
// Application window width/height to center our window
// _, h = shmem.CurrentRenderEngine.WindowSize()
)
win := NewCheatsWindow(cfg)
win.Compute(cfg.Engine)
win.Supervise(cfg.Supervisor)
// Center the window.
// size := win.Size()
win.MoveTo(render.Point{
X: 20,
Y: 40,
})
return win
}
// NewCheatsWindow initializes the window.
func NewCheatsWindow(cfg CheatsMenu) *ui.Window {
var (
Width = 200
Height = 300
)
window := ui.NewWindow("Cheats Menu")
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: Width,
Height: Height,
Background: balance.CheatsMenuBackground,
})
///////////
// Tab Bar
tabFrame := ui.NewTabFrame("Tab Frame")
tabFrame.SetBackground(balance.CheatsMenuBackground)
window.Pack(tabFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
// Make the tabs
cfg.makePlayModeTab(tabFrame, Width, Height)
cfg.makeMiscTab(tabFrame, Width, Height)
tabFrame.Supervise(cfg.Supervisor)
return window
}
// Cheats Menu "Play Mode" Tab
func (c CheatsMenu) makePlayModeTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
tab := tabFrame.AddTab("Gameplay", ui.NewLabel(ui.Label{
Text: "Gameplay",
Font: balance.TabFont,
}))
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
// Run a command on the developer shell.
run := func(command string) {
if c.RunCommand != nil {
c.RunCommand(command)
} else {
shmem.FlashError("CheatsMenu: RunCommand() handler not available")
}
}
// Dummy variable for the "play as" dropdown.
var playAs string
form := magicform.Form{
Supervisor: c.Supervisor,
Engine: c.Engine,
Vertical: true,
LabelWidth: 90,
PadY: 0,
PadX: 0,
}
form.Create(tab, []magicform.Field{
{
Label: "These cheats are available\n" +
"only during level gameplay.",
Font: balance.UIFont,
},
{
Label: "Play as:",
TextVariable: &playAs,
Options: balance.CheatMenuActors,
Font: balance.UIFont,
OnSelect: func(v interface{}) {
doodad := v.(string)
if c.OnSetPlayerCharacter != nil {
c.OnSetPlayerCharacter(doodad)
} else {
shmem.FlashError("OnSetPlayerCharacter(%s): handler not ready", doodad)
}
},
},
{
Buttons: []magicform.Field{
{
Label: "God Mode",
Font: balance.SmallFont,
ButtonStyle: &balance.ButtonDanger,
Tooltip: ui.Tooltip{
Text: "Makes you invulnerable to damage and fire.",
},
OnClick: func() {
run(balance.CheatGodMode)
},
},
{
Label: "Show hidden actors",
Font: balance.SmallFont,
OnClick: func() {
run(balance.CheatShowAllActors)
},
},
},
},
{
Label: "Inventory",
Font: balance.LabelFont,
},
{
Buttons: []magicform.Field{
{
Label: "Give Keys",
Font: balance.SmallFont,
Tooltip: ui.Tooltip{
Text: "Get all four colored keys and\n99x small keys",
},
OnClick: func() {
run(balance.CheatGiveKeys)
},
},
{
Label: "Give Gems",
Font: balance.SmallFont,
Tooltip: ui.Tooltip{
Text: "Get 1x of each of the four Gemstones.",
},
OnClick: func() {
run(balance.CheatGiveGems)
},
},
{
Label: "Drop All",
Font: balance.SmallFont,
ButtonStyle: &balance.ButtonDanger,
Tooltip: ui.Tooltip{
Text: "Remove ALL items from your inventory.",
},
OnClick: func() {
run(balance.CheatDropItems)
},
},
},
},
{
Label: "Physics",
Font: balance.LabelFont,
},
{
Buttons: []magicform.Field{
{
Label: "Antigravity",
Font: balance.SmallFont,
Tooltip: ui.Tooltip{
Text: "Allows free movement in four directions",
},
OnClick: func() {
run(balance.CheatAntigravity)
},
},
{
Label: "NoClip",
Font: balance.SmallFont,
Tooltip: ui.Tooltip{
Text: "Toggle physical collision\n" +
"checks with level and actors.",
},
OnClick: func() {
run(balance.CheatNoclip)
},
},
},
},
{
Buttons: []magicform.Field{
{
Label: "Skip this level",
Font: balance.SmallFont,
ButtonStyle: &balance.ButtonDanger,
Tooltip: ui.Tooltip{
Text: "Consider the current level a win.",
},
OnClick: func() {
run(balance.CheatSkipLevel)
},
},
},
},
})
return tab
}
// Cheats Menu "Misc" Tab
func (c CheatsMenu) makeMiscTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
tab := tabFrame.AddTab("Misc", ui.NewLabel(ui.Label{
Text: "Misc",
Font: balance.TabFont,
}))
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
// Run a command on the developer shell.
run := func(command string) {
if c.RunCommand != nil {
c.RunCommand(command)
} else {
shmem.FlashError("CheatsMenu: RunCommand() handler not available")
}
}
form := magicform.Form{
Supervisor: c.Supervisor,
Engine: c.Engine,
Vertical: true,
LabelWidth: 90,
PadY: 0,
PadX: 0,
}
form.Create(tab, []magicform.Field{
{
Label: "Enable cheats menu",
BoolVariable: &usercfg.Current.EnableCheatsMenu,
Tooltip: ui.Tooltip{
Text: "Enables a Help->Cheats Menu during gameplay.",
},
OnClick: func() {
saveGameSettings()
},
},
{
Label: "Level Editor",
Font: balance.LabelFont,
},
{
Buttons: []magicform.Field{
{
Label: "Show hidden doodads",
Font: balance.SmallFont,
Tooltip: ui.Tooltip{
Text: "Enable hidden built-in doodads (such as Boy)\n" +
"to be used in the Level Editor.",
Edge: ui.Bottom,
},
OnClick: func() {
// Like `boolProp show-hidden-doodads true`
var bp = "show-hidden-doodads"
if v, err := balance.GetBoolProp(bp); err == nil {
v = !v
balance.BoolProp(bp, v)
if v {
shmem.Flash("Hidden doodads will appear when you next reload the level editor.")
} else {
shmem.Flash("Hidden doodads are again hidden from the level editor.")
}
}
},
},
},
},
{
Label: "Testing",
Font: balance.LabelFont,
},
{
Buttons: []magicform.Field{
{
Label: "Load Screen",
Font: balance.SmallFont,
OnClick: func() {
run(balance.CheatDebugLoadScreen)
},
},
{
Label: "Wait Screen",
Font: balance.SmallFont,
OnClick: func() {
run(balance.CheatDebugWaitScreen)
},
},
},
},
{
Label: "Level Progression",
Font: balance.LabelFont,
},
{
Buttons: []magicform.Field{
{
Label: "Unlock all levels",
Font: balance.SmallFont,
Tooltip: ui.Tooltip{
Text: "For this play session, any level may be opened\n" +
"from Story Mode regardless of the padlock icon.",
},
OnClick: func() {
run(balance.CheatUnlockLevels)
},
},
},
},
{
Label: "Debugging",
Font: balance.LabelFont,
},
{
Buttons: []magicform.Field{
{
Label: "Debug overlay",
Font: balance.SmallFont,
OnClick: func() {
run("boolprop DO flip")
},
},
{
Label: "Show hitboxes",
Font: balance.SmallFont,
OnClick: func() {
run("boolprop DC flip")
},
},
},
},
})
return tab
}

View File

@ -7,6 +7,7 @@ import (
"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"
@ -32,9 +33,13 @@ type Settings struct {
ControllerStyle *int
// Configuration options.
SceneName string // name of scene which called this window
ActiveTab string // specify the tab to open
OnApply func()
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.
@ -452,6 +457,42 @@ func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int)
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