Noah Petherbridge
fb5a8a1ae8
* 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
400 lines
8.8 KiB
Go
400 lines
8.8 KiB
Go
package doodle
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
|
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
|
|
"git.kirsle.net/apps/doodle/pkg/physics"
|
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/render/event"
|
|
"git.kirsle.net/go/ui"
|
|
"github.com/robertkrimen/otto"
|
|
)
|
|
|
|
// Flash a message to the user.
|
|
func (d *Doodle) Flash(template string, v ...interface{}) {
|
|
log.Warn(template, v...)
|
|
d.shell.Write(fmt.Sprintf(template, v...))
|
|
}
|
|
|
|
// FlashError flashes an error-colored message to the user.
|
|
func (d *Doodle) FlashError(template string, v ...interface{}) {
|
|
log.Error(template, v...)
|
|
d.shell.WriteColorful(fmt.Sprintf(template, v...), balance.FlashErrorColor)
|
|
}
|
|
|
|
// Prompt the user for a question in the dev console.
|
|
func (d *Doodle) Prompt(question string, callback func(string)) {
|
|
d.shell.Prompt = question
|
|
d.shell.callback = callback
|
|
d.shell.Open = true
|
|
}
|
|
|
|
// Shell implements the developer console in-game.
|
|
type Shell struct {
|
|
parent *Doodle
|
|
|
|
Open bool
|
|
Prompt string
|
|
Repl bool
|
|
callback func(string) // for prompt answers only
|
|
Text string
|
|
History []string
|
|
Output []string
|
|
Flashes []Flash
|
|
|
|
// Blinky cursor variables.
|
|
cursor byte // cursor symbol
|
|
cursorFlip uint64 // ticks until cursor flip
|
|
cursorRate uint64
|
|
|
|
// Paging through history variables.
|
|
historyPaging bool
|
|
historyIndex int
|
|
|
|
// JavaScript shell interpreter.
|
|
js *otto.Otto
|
|
}
|
|
|
|
// Flash holds a message to flash on screen.
|
|
type Flash struct {
|
|
Text string
|
|
Expires uint64 // tick that it expires
|
|
Color render.Color
|
|
}
|
|
|
|
// NewShell initializes the shell helper (the "Shellper").
|
|
func NewShell(d *Doodle) Shell {
|
|
s := Shell{
|
|
parent: d,
|
|
History: []string{},
|
|
Output: []string{},
|
|
Flashes: []Flash{},
|
|
Prompt: ">",
|
|
cursor: '_',
|
|
cursorRate: balance.ShellCursorBlinkRate,
|
|
js: otto.New(),
|
|
}
|
|
|
|
// Make the Doodle instance available to the shell.
|
|
bindings := map[string]interface{}{
|
|
"d": d,
|
|
"Execute": s.Execute,
|
|
"RGBA": render.RGBA,
|
|
"Point": render.NewPoint,
|
|
"Vector": physics.NewVector,
|
|
"Rect": render.NewRect,
|
|
"Tree": func(w ui.Widget) string {
|
|
for _, row := range ui.WidgetTree(w) {
|
|
d.Flash(row)
|
|
}
|
|
return ""
|
|
},
|
|
"loadscreen": map[string]interface{}{
|
|
"Show": loadscreen.Show,
|
|
"ShowWithProgress": loadscreen.ShowWithProgress,
|
|
"Hide": loadscreen.Hide,
|
|
"IsActive": loadscreen.IsActive,
|
|
"SetProgress": loadscreen.SetProgress,
|
|
},
|
|
}
|
|
for name, v := range bindings {
|
|
err := s.js.Set(name, v)
|
|
if err != nil {
|
|
log.Error("Failed to make `%s` available to JS shell: %s", name, err)
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Close the shell, resetting its internal state.
|
|
func (s *Shell) Close() {
|
|
log.Debug("Shell: closing shell")
|
|
s.Open = false
|
|
s.Repl = false
|
|
s.Prompt = ">"
|
|
s.callback = nil
|
|
s.Text = ""
|
|
s.historyPaging = false
|
|
s.historyIndex = 0
|
|
}
|
|
|
|
// Execute a command in the shell.
|
|
func (s *Shell) Execute(input string) {
|
|
command := s.Parse(input)
|
|
|
|
if command.Raw != "" {
|
|
s.Output = append(s.Output, s.Prompt+command.Raw)
|
|
s.History = append(s.History, command.Raw)
|
|
}
|
|
|
|
// Are we answering a Prompt?
|
|
if s.callback != nil {
|
|
log.Info("Invoking prompt callback:")
|
|
s.callback(command.Raw)
|
|
s.Close()
|
|
return
|
|
}
|
|
|
|
if command.Command == "clear" {
|
|
s.Output = []string{}
|
|
} else {
|
|
err := command.Run(s.parent)
|
|
if err != nil {
|
|
s.Write(err.Error())
|
|
}
|
|
}
|
|
|
|
// Reset the text buffer in the shell.
|
|
if s.Repl {
|
|
s.Text = "$ "
|
|
} else {
|
|
s.Text = ""
|
|
}
|
|
}
|
|
|
|
// Write a line of output text to the console.
|
|
func (s *Shell) Write(line string) {
|
|
s.Output = append(s.Output, line)
|
|
s.Flashes = append(s.Flashes, Flash{
|
|
Text: line,
|
|
Expires: shmem.Tick + balance.FlashTTL,
|
|
})
|
|
}
|
|
|
|
// WriteError writes a line of error (red) text to the console.
|
|
func (s *Shell) WriteColorful(line string, color render.Color) {
|
|
s.Output = append(s.Output, line)
|
|
s.Flashes = append(s.Flashes, Flash{
|
|
Text: line,
|
|
Color: color,
|
|
Expires: shmem.Tick + balance.FlashTTL,
|
|
})
|
|
}
|
|
|
|
// Parse the command line.
|
|
func (s *Shell) Parse(input string) Command {
|
|
input = strings.TrimSpace(input)
|
|
if len(input) == 0 {
|
|
return Command{}
|
|
}
|
|
|
|
var (
|
|
inQuote bool
|
|
buffer = bytes.NewBuffer([]byte{})
|
|
words = []string{}
|
|
)
|
|
for i := 0; i < len(input); i++ {
|
|
char := input[i]
|
|
switch char {
|
|
case ' ':
|
|
if inQuote {
|
|
buffer.WriteByte(char)
|
|
continue
|
|
}
|
|
|
|
if word := buffer.String(); word != "" {
|
|
words = append(words, word)
|
|
buffer.Reset()
|
|
}
|
|
case '"':
|
|
if !inQuote {
|
|
// An opening quote character.
|
|
inQuote = true
|
|
} else {
|
|
// The closing quote.
|
|
inQuote = false
|
|
|
|
if word := buffer.String(); word != "" {
|
|
words = append(words, word)
|
|
buffer.Reset()
|
|
}
|
|
}
|
|
default:
|
|
buffer.WriteByte(char)
|
|
}
|
|
}
|
|
|
|
if remainder := buffer.String(); remainder != "" {
|
|
words = append(words, remainder)
|
|
}
|
|
|
|
return Command{
|
|
Raw: input,
|
|
Command: words[0],
|
|
Args: words[1:],
|
|
ArgsLiteral: strings.TrimSpace(input[len(words[0]):]),
|
|
}
|
|
}
|
|
|
|
// Draw the shell.
|
|
func (s *Shell) Draw(d *Doodle, ev *event.State) error {
|
|
// Compute the line height we can draw.
|
|
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
|
|
|
|
// If the console is open, draw the console.
|
|
if s.Open {
|
|
if ev.Escape {
|
|
s.Close()
|
|
return nil
|
|
} else if keybind.Enter(ev) {
|
|
s.Execute(s.Text)
|
|
|
|
// Auto-close the console unless in REPL mode.
|
|
if !s.Repl {
|
|
s.Close()
|
|
}
|
|
|
|
return nil
|
|
} else if (ev.Up || ev.Down) && len(s.History) > 0 {
|
|
// Paging through history.
|
|
if !s.historyPaging {
|
|
s.historyPaging = true
|
|
s.historyIndex = len(s.History)
|
|
}
|
|
|
|
// Consume the inputs and make convenient variables.
|
|
isUp := ev.Up
|
|
ev.Down = false
|
|
ev.Up = false
|
|
|
|
// Scroll through the input history.
|
|
if isUp {
|
|
s.historyIndex--
|
|
if s.historyIndex < 0 {
|
|
s.historyIndex = 0
|
|
}
|
|
} else {
|
|
s.historyIndex++
|
|
if s.historyIndex >= len(s.History) {
|
|
s.historyIndex = len(s.History) - 1
|
|
}
|
|
}
|
|
|
|
s.Text = s.History[s.historyIndex]
|
|
|
|
}
|
|
|
|
// Cursor flip?
|
|
if shmem.Tick > s.cursorFlip {
|
|
s.cursorFlip = shmem.Tick + s.cursorRate
|
|
if s.cursor == ' ' {
|
|
s.cursor = '_'
|
|
} else {
|
|
s.cursor = ' '
|
|
}
|
|
}
|
|
|
|
// Read a character from the keyboard.
|
|
for _, key := range ev.KeysDown(true) {
|
|
// Backspace?
|
|
if key == `\b` {
|
|
if len(s.Text) > 0 {
|
|
s.Text = s.Text[:len(s.Text)-1]
|
|
}
|
|
} else {
|
|
s.Text += key
|
|
}
|
|
// HACK: I wanted to do:
|
|
// ev.SetKeyDown(key, false)
|
|
// But, ev.KeysDown(shifted=true) returns letter keys
|
|
// like 'M' when the key we wanted to unset was 'm',
|
|
// or we got '$' when we want to unset '5'... so all
|
|
// shifted chars got duplicated 3+ times on key press!
|
|
// So, just reset ALL key press states to work around it:
|
|
ev.ResetKeyDown()
|
|
}
|
|
|
|
// How tall is the box?
|
|
boxHeight := (lineHeight * (balance.ShellHistoryLineCount + 1)) + balance.ShellPadding
|
|
|
|
// Draw the background color.
|
|
d.Engine.DrawBox(
|
|
balance.ShellBackgroundColor,
|
|
render.Rect{
|
|
X: 0,
|
|
Y: d.height - boxHeight,
|
|
W: d.width,
|
|
H: boxHeight,
|
|
},
|
|
)
|
|
|
|
// Draw the recent commands.
|
|
outputY := d.height - (lineHeight * 2)
|
|
for i := 0; i < balance.ShellHistoryLineCount; i++ {
|
|
if len(s.Output) > i {
|
|
line := s.Output[len(s.Output)-1-i]
|
|
d.Engine.DrawText(
|
|
render.Text{
|
|
FontFilename: balance.ShellFontFilename,
|
|
Text: line,
|
|
Size: balance.ShellFontSize,
|
|
Color: balance.ShellForegroundColor,
|
|
},
|
|
render.Point{
|
|
X: balance.ShellPadding,
|
|
Y: outputY,
|
|
},
|
|
)
|
|
}
|
|
outputY -= lineHeight
|
|
}
|
|
|
|
// Draw the command prompt.
|
|
d.Engine.DrawText(
|
|
render.Text{
|
|
FontFilename: balance.ShellFontFilename,
|
|
Text: s.Prompt + s.Text + string(s.cursor),
|
|
Size: balance.ShellFontSize,
|
|
Color: balance.ShellPromptColor,
|
|
},
|
|
render.Point{
|
|
X: balance.ShellPadding,
|
|
Y: d.height - balance.ShellFontSize - balance.ShellPadding,
|
|
},
|
|
)
|
|
} else if len(s.Flashes) > 0 {
|
|
// Otherwise, just draw flashed messages.
|
|
valid := false // Did we actually draw any?
|
|
|
|
outputY := d.height - (lineHeight * 2) - 16
|
|
for i := len(s.Flashes); i > 0; i-- {
|
|
flash := s.Flashes[i-1]
|
|
if shmem.Tick >= flash.Expires {
|
|
continue
|
|
}
|
|
|
|
var text = balance.FlashFont(flash.Text)
|
|
if !flash.Color.IsZero() {
|
|
text.Color = flash.Color
|
|
text.Stroke = text.Color.Darken(balance.FlashStrokeDarken)
|
|
text.Shadow = text.Color.Darken(balance.FlashShadowDarken)
|
|
}
|
|
|
|
d.Engine.DrawText(
|
|
text,
|
|
render.Point{
|
|
X: balance.ShellPadding + toolbarWidth,
|
|
Y: outputY,
|
|
},
|
|
)
|
|
outputY -= lineHeight
|
|
valid = true
|
|
}
|
|
|
|
// If we've exhausted all flashes, free up the memory.
|
|
if !valid {
|
|
s.Flashes = []Flash{}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|