doodle/shell.go
Noah Petherbridge e141203c4b Basic Collision Detection, Toggle Between Play/Edit
Known bugs:
* The Pixel format in the Grid has DX and DY attributes and
  it wreaks havoc on collision detection in Play Mode when you
  come straight from the editor. Reloading the map from disk to
  play is OK cuz it lacks these attrs.
2018-07-23 20:10:53 -07:00

259 lines
5.2 KiB
Go

package doodle
import (
"bytes"
"fmt"
"strings"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
)
// Flash a message to the user.
func (d *Doodle) Flash(template string, v ...interface{}) {
d.shell.Write(fmt.Sprintf(template, v...))
}
// Shell implements the developer console in-game.
type Shell struct {
parent *Doodle
Open bool
Prompt string
Text string
History []string
Output []string
Flashes []Flash
Cursor string
cursorFlip uint64 // ticks until cursor flip
cursorRate uint64
}
// Flash holds a message to flash on screen.
type Flash struct {
Text string
Expires uint64 // tick that it expires
}
// NewShell initializes the shell helper (the "Shellper").
func NewShell(d *Doodle) Shell {
return Shell{
parent: d,
History: []string{},
Output: []string{},
Flashes: []Flash{},
Prompt: ">",
Cursor: "_",
cursorRate: balance.ShellCursorBlinkRate,
}
}
// Close the shell, resetting its internal state.
func (s *Shell) Close() {
log.Debug("Shell: closing shell")
s.Open = false
s.Prompt = ">"
s.Text = ""
}
// Execute a command in the shell.
func (s *Shell) Execute(input string) {
command := s.Parse(input)
err := command.Run(s.parent)
if err != nil {
s.Write(err.Error())
}
if command.Raw != "" {
s.History = append(s.History, command.Raw)
}
// Reset the text buffer in the shell.
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: s.parent.ticks + 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 *events.State) error {
if ev.EscapeKey.Read() {
s.Close()
return nil
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
s.Execute(s.Text)
s.Close()
return nil
}
// Compute the line height we can draw.
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
// If the console is open, draw the console.
if s.Open {
// Cursor flip?
if d.ticks > s.cursorFlip {
s.cursorFlip = d.ticks + s.cursorRate
if s.Cursor == "" {
s.Cursor = "_"
} else {
s.Cursor = ""
}
}
// Read a character from the keyboard.
if key := ev.ReadKey(); key != "" {
// Backspace?
if key == `\b` {
if len(s.Text) > 0 {
s.Text = s.Text[:len(s.Text)-1]
}
} else {
s.Text += key
}
}
// How tall is the box?
boxHeight := int32(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 - int32(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{
Text: line,
Size: balance.ShellFontSize,
Color: render.Grey,
},
render.Point{
X: balance.ShellPadding,
Y: outputY,
},
)
}
outputY -= int32(lineHeight)
}
// Draw the command prompt.
d.Engine.DrawText(
render.Text{
Text: s.Prompt + s.Text + s.Cursor,
Size: balance.ShellFontSize,
Color: balance.ShellForegroundColor,
},
render.Point{
X: balance.ShellPadding,
Y: d.height - int32(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 - int32(lineHeight*2)
for i := len(s.Flashes); i > 0; i-- {
flash := s.Flashes[i-1]
if d.ticks >= flash.Expires {
continue
}
d.Engine.DrawText(
render.Text{
Text: flash.Text,
Size: balance.ShellFontSize,
Color: render.SkyBlue,
Stroke: render.Grey,
Shadow: render.Black,
},
render.Point{
X: balance.ShellPadding,
Y: outputY,
},
)
outputY -= int32(lineHeight)
valid = true
}
// If we've exhausted all flashes, free up the memory.
if !valid {
s.Flashes = []Flash{}
}
}
return nil
}