doodle/pkg/commands.go
Noah Petherbridge fb5a8a1ae8 Async Giant Screenshot, Player Physics and UI Polish
* The "Giant Screenshot" feature takes a very long time, so it is made
  asynchronous. If you try and run a second one while the first is busy,
  you get an error flash. You can continue editing the level, even
  playtest it, or load a different level, and it will continue crunching
  on the Giant Screenshot and flash when it's finished.
* Updated the player physics to use proper Velocity to jump off the
  ground rather than the hacky timer-based fixed speed approach.
* FlashError() function to flash "error level" messages to the screen.
  They appear in orange text instead of the usual blue, and most error
  messages in the game use this now. The dev console "error <msg>"
  command can simulate an error message.
* Flashed message fonts are updated. The blue font now uses softer
  stroke and shadow colors and the same algorithm applies to the orange
  error flashes.

Some other changes to player physics:

* Max velocity, acceleration speed, and gravity have been tweaked.
* Fast turn-around if you are moving right and then need to go left.
  Your velocity resets to zero at the transition so you quickly get
  going the way you want to go.

Some levels that need a bit of love for the new platforming physics:

* Tutorial 3.level
2021-10-07 18:27:38 -07:00

329 lines
7.3 KiB
Go

package doodle
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"git.kirsle.net/apps/doodle/assets"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal"
"github.com/robertkrimen/otto"
)
// Command is a parsed shell command.
type Command struct {
Raw string // The complete raw command the user typed.
Command string // The first word of their command.
Args []string // The shell-args array of parameters.
ArgsLiteral string // The args portion of the command literally.
}
// Run the command.
func (c Command) Run(d *Doodle) error {
if len(c.Raw) == 0 {
return nil
}
// Cheat codes
if cheat := c.cheatCommand(d); cheat {
return nil
}
switch strings.ToLower(c.Command) {
case "echo":
d.Flash(c.ArgsLiteral)
return nil
case "error":
d.FlashError(c.ArgsLiteral)
return nil
case "alert":
modal.Alert(c.ArgsLiteral)
return nil
case "confirm":
modal.Confirm(c.ArgsLiteral).Then(func() {
d.Flash("Confirmed.")
})
return nil
case "new":
return c.New(d)
case "save":
return c.Save(d)
case "edit":
return c.Edit(d)
case "play":
return c.Play(d)
case "close":
return c.Close(d)
case "exit":
case "quit":
return c.Quit()
case "help":
return c.Help(d)
case "reload":
d.Goto(d.Scene)
return nil
case "guitest":
d.Goto(&GUITestScene{})
return nil
case "eval":
fallthrough
case "$":
out, err := c.RunScript(d, c.ArgsLiteral)
d.Flash("%+v", out)
return err
case "repl":
d.shell.Repl = true
d.shell.Text = "$ "
case "boolprop":
return c.BoolProp(d)
case "extract-bindata":
// Undocumented command to extract the binary of its assets.
return c.ExtractBindata(d, c.ArgsLiteral)
default:
return c.Default()
}
return nil
}
// New opens a new map in the editor mode.
func (c Command) New(d *Doodle) error {
d.GotoNewMenu()
return nil
}
// Close returns to the Main Scene.
func (c Command) Close(d *Doodle) error {
main := &MainScene{}
d.Goto(main)
return nil
}
// ExtractBindata dumps the app's embedded bindata to the filesystem.
func (c Command) ExtractBindata(d *Doodle, path string) error {
if len(path) == 0 || path[0] != '/' {
d.FlashError("Required: an absolute path to a directory to extract to.")
return nil
}
err := os.MkdirAll(path, 0755)
if err != nil {
d.FlashError("MkdirAll: %s", err)
return err
}
for _, filename := range assets.AssetNames() {
outfile := filepath.Join(path, filename)
log.Info("Extracting bindata: %s to: %s", filename, outfile)
data, err := assets.Asset(filename)
if err != nil {
d.FlashError("error on file %s: %s", filename, err)
continue
}
// Fill out the directory path.
if _, err := os.Stat(filepath.Dir(outfile)); os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(outfile), 0755)
}
fh, err := os.Create(outfile)
if err != nil {
d.FlashError("error writing file %s: %s", outfile, err)
continue
}
fh.Write(data)
fh.Close()
}
d.Flash("Bindata extracted to %s", path)
return nil
}
// Help prints the help info.
func (c Command) Help(d *Doodle) error {
if len(c.Args) == 0 {
d.Flash("Available commands: new save edit play quit echo error")
d.Flash(" alert clear help boolProp eval repl")
d.Flash("Type `help` and then the command, like: `help edit`")
return nil
}
switch strings.ToLower(c.Args[0]) {
case "echo":
d.Flash("Usage: echo <message>")
d.Flash("Flash a message back to the console")
case "error":
d.Flash("Usage: error <message>")
d.Flash("Flash an error message back to the console")
case "alert":
d.Flash("Usage: alert <message>")
d.Flash("Pop up an Alert box with a custom message")
case "new":
d.Flash("Usage: new")
d.Flash("Create a new drawing in Edit Mode")
case "save":
d.Flash("Usage: save [filename.json]")
d.Flash("Save the map to disk (in Edit Mode only)")
case "edit":
d.Flash("Usage: edit <filename.json>")
d.Flash("Open a file on disk in Edit Mode")
case "play":
d.Flash("Usage: play <filename.json>")
d.Flash("Open a map from disk in Play Mode")
case "quit":
fallthrough
case "exit":
d.Flash("Usage: quit")
d.Flash("Closes the dev console (alias: exit)")
case "clear":
d.Flash("Usage: clear")
d.Flash("Clears the console output history")
case "eval":
fallthrough
case "$":
d.Flash("Evaluate a line of JavaScript on the in-game interpreter")
case "repl":
d.Flash("Enter a JavaScript shell on the in-game interpreter")
case "boolprop":
d.Flash("Toggle boolean values. `boolProp list` lists available")
case "help":
d.Flash("Usage: help <command>")
d.Flash("Gets further help on a command")
default:
d.Flash("Unknown help topic.")
}
return nil
}
// Save the current map to disk.
func (c Command) Save(d *Doodle) error {
if scene, ok := d.Scene.(*EditorScene); ok {
filename := ""
if len(c.Args) > 0 {
filename = c.Args[0]
} else if scene.filename != "" {
filename = scene.filename
} else {
return errors.New("usage: save <filename>")
}
switch scene.DrawingType {
case enum.LevelDrawing:
d.shell.Write("Saving Level: " + filename)
scene.SaveLevel(filename)
case enum.DoodadDrawing:
d.shell.Write("Saving Doodad: " + filename)
scene.SaveDoodad(filename)
}
} else {
return errors.New("save: only available in Edit Mode")
}
return nil
}
// Edit a map from disk.
func (c Command) Edit(d *Doodle) error {
if len(c.Args) == 0 {
return errors.New("Usage: edit <file name>")
}
filename := c.Args[0]
d.shell.Write("Editing file: " + filename)
return d.EditFile(filename)
}
// Play a map.
func (c Command) Play(d *Doodle) error {
if len(c.Args) == 0 {
return errors.New("Usage: play <file name>")
}
filename := c.Args[0]
d.shell.Write("Playing level: " + filename)
d.PlayLevel(filename)
return nil
}
// Quit the command line shell.
func (c Command) Quit() error {
return nil
}
// BoolProp command sets available boolean variables.
func (c Command) BoolProp(d *Doodle) error {
if len(c.Args) == 1 {
// Showing the value of a boolProp. Only supported for those defined
// in balance/boolprops.go
value, err := balance.GetBoolProp(c.Args[0])
if err != nil {
return err
}
d.Flash("%s: %+v", c.Args[0], value)
return nil
}
if len(c.Args) != 2 {
return errors.New("Usage: boolProp <name> [true or false]")
}
var (
name = c.Args[0]
value = c.Args[1]
truthy = value[0] == 't' || value[0] == 'T' || value[0] == '1'
ok = true
)
switch name {
case "Debug":
case "D":
d.Debug = truthy
case "DebugOverlay":
case "DO":
DebugOverlay = truthy
case "DebugCollision":
case "DC":
DebugCollision = truthy
default:
ok = false
}
if ok {
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 {
d.FlashError("%s", err)
} else {
d.Flash("%s: %+v", name, truthy)
}
}
return nil
}
// RunScript evaluates some JavaScript code safely.
func (c Command) RunScript(d *Doodle, code interface{}) (otto.Value, error) {
defer func() {
if err := recover(); err != nil {
d.FlashError("Command.RunScript: Panic: %s", err)
}
}()
out, err := d.shell.js.Run(code)
return out, err
}
// Default command.
func (c Command) Default() error {
return fmt.Errorf("%s: command not found. Try `help` for help",
c.Command,
)
}