Add JS + History to Shell, Add Main Scene

* The shell now supports an "eval" command, or "$" for short.
  * Runs it in an Otto JavaScript VM.
  * Some global variables are available, like `d` is the Doodle object
    itself, `log`, `RGBA()` and `Point()`
* The shell supports paging through input history using the arrow keys.
* Added an initial Main Scene
chunks
Noah 2018-07-25 19:38:54 -07:00
parent 94c1df050b
commit 41e1838549
12 changed files with 327 additions and 112 deletions

46
Debug Notes.md Normal file
View File

@ -0,0 +1,46 @@
# Debug Notes
## Entering Debug Mode
Command line argument:
```bash
% doodle -debug
# running the dev build uses debug mode by default
% make run
```
In the developer console:
```dos
> boolProp Debug true
> boolProp D true
```
## Debug Options
The `boolProp` command can also be used to toggle on and off different
debug options while the game is running.
```
DebugOverlay
DO
Toggles the main debug text overlay with FPS counter.
DebugCollision
DC
Toggles the collision detection bounding box lines.
```
## JavaScript Shell
The developer console can parse JavaScript commands for more access to the
game's internal objects.
The following global variables are available to the shell:
* `d` is the master Doodle struct.
* `log` is the master logger object for logging messages to the terminal.
* `RGBA()` is the `render.RGBA()` function for creating a Color value.
* `Point(x, y)` to create a `render.Point`

View File

@ -139,17 +139,54 @@ As a rough idea of the milestones needed for this game to work:
for the rest of this program.
* [x] Labels
* [ ] Buttons (text only is OK)
* [x] Buttons wrap their Label and dynamically compute their size based
on how wide the label will render, plus padding and border.
* [x] Border colors and widths and paddings are all configurable.
* [ ] Buttons should interact with the cursor and be hoverable and
clickable.
* [x] Buttons wrap their Label and dynamically compute their size based
on how wide the label will render, plus padding and border.
* [x] Border colors and widths and paddings are all configurable.
* [ ] Buttons should interact with the cursor and be hoverable and
clickable.
* [ ] UI Manager that will keep track of buttons to know when the mouse
is interacting with them.
* [ ] Frames
* Like Buttons, can have border (raised, sunken or solid), padding and
background color.
* [ ] Should be able to size themselves dynamically based on child widgets.
* [ ] Windows (fixed, non-draggable is OK)
* [ ] Expand the Palette support in levels for solid vs. transparent, fire,
etc. with UI toolbar to choose palettes.
* [ ] Title bar with label
* [ ] Window body implements a Frame that contains child widgets.
* [ ] Window can resize itself dynamically based on the Frame.
* [ ] Create a "Main Menu" scene with buttons to enter a new Edit Mode,
play an existing map from disk, etc.
* [ ] Add user interface Frames or Windows to the Edit Mode.
* [ ] A toolbar of buttons (New, Save, Open, Play) can be drawn at the top
before the UI toolkit gains a proper MenuBar widget.
* [ ] Expand the Palette support in levels for solid vs. transparent, fire,
etc. with UI toolbar to choose palettes.
Lesser important UI features that can come at any later time:
* [ ] MenuBar widget with drop-down menu support.
* [ ] Checkbox and Radiobox widgets.
* [ ] Text Entry widgets (in the meantime use the Developer Shell to prompt for
text input questions)
## Doodad Editor
* [ ] The Edit Mode should support creating drawings for Doodads.
* [ ] It should know whether you're drawing a Map or a Doodad as some
behaviors may need to be different between the two.
* [ ] Compress the coordinates down toward `(0,0)` when saving a Doodad,
by finding the toppest, leftest point and making that `(0,0)` and adjusting
the rest accordingly. This will help trim down Doodads into the smallest
possible space for easy collision detection.
* [ ] Add a UX to edit multiple frames for a Doodad.
* [ ] Edit Mode should be able to fully save the drawings and frames, and an
external CLI tool can install the JavaScript into them.
* [ ] Possible UX to toggle Doodad options, like its collision rules and
whether the Doodad is continued to be "mobile" (i.e. doors and buttons won't
move, but items and enemies may be able to; and non-mobile Doodads don't
need to collision check against level geometry).
* [ ] Edit Mode should have a Doodad Palette (Frame or Window) to drag
Doodads into the map.
* [ ] ???
# Building

View File

@ -3,6 +3,7 @@ package doodle
import (
"errors"
"fmt"
"strconv"
)
// Command is a parsed shell command.
@ -36,6 +37,13 @@ func (c Command) Run(d *Doodle) error {
return c.Quit()
case "help":
return c.Help(d)
case "eval":
case "$":
out, err := d.shell.js.Run(c.ArgsLiteral)
d.Flash("%+v", out)
return err
case "boolProp":
return c.BoolProp(d)
default:
return c.Default()
}
@ -91,7 +99,7 @@ func (c Command) Help(d *Doodle) error {
// Save the current map to disk.
func (c Command) Save(d *Doodle) error {
if scene, ok := d.scene.(*EditorScene); ok {
if scene, ok := d.Scene.(*EditorScene); ok {
filename := ""
if len(c.Args) > 0 {
filename = c.Args[0]
@ -139,6 +147,42 @@ func (c Command) Quit() error {
return nil
}
// BoolProp command sets available boolean variables.
func (c Command) BoolProp(d *Doodle) error {
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 {
d.Flash("Unknown boolProp name %s", name)
}
return nil
}
// Default command.
func (c Command) Default() error {
return fmt.Errorf("%s: command not found. Try `help` for help",

View File

@ -32,7 +32,7 @@ type Doodle struct {
// Command line shell options.
shell Shell
scene Scene
Scene Scene
}
// New initializes the game object.
@ -62,8 +62,8 @@ func (d *Doodle) Run() error {
}
// Set up the default scene.
if d.scene == nil {
d.Goto(&EditorScene{})
if d.Scene == nil {
d.Goto(&MainScene{})
}
log.Info("Enter Main Loop")
@ -96,14 +96,14 @@ func (d *Doodle) Run() error {
}
// Run the scene's logic.
err = d.scene.Loop(d, ev)
err = d.Scene.Loop(d, ev)
if err != nil {
return err
}
}
// Draw the scene.
d.scene.Draw(d)
d.Scene.Draw(d)
// Draw the shell.
err = d.shell.Draw(d, ev)
@ -114,7 +114,7 @@ func (d *Doodle) Run() error {
}
// Draw the debug overlay over all scenes.
// d.DrawDebugOverlay()
d.DrawDebugOverlay()
// Render the pixels to the screen.
err = d.Engine.Present()

View File

@ -11,7 +11,6 @@ import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// EditorScene manages the "Edit Level" game mode.
@ -133,59 +132,6 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
func (s *EditorScene) Draw(d *Doodle) error {
s.canvas.Draw(d.Engine)
label := ui.NewLabel(render.Text{
Text: "Hello UI toolkit!",
Size: 26,
Color: render.Pink,
Stroke: render.SkyBlue,
Shadow: render.Black,
})
label.SetPoint(render.NewPoint(128, 128))
label.Compute(d.Engine)
log.Info("Label rect: %+v", label.Size())
log.Info("Label at: %s", label.Point())
label.Present(d.Engine)
button := ui.NewButton(*ui.NewLabel(render.Text{
Text: "Hello",
Size: 14,
Color: render.Black,
}))
button.SetPoint(render.NewPoint(200, 200))
button.Present(d.Engine)
// Point and size of that button
point := button.Point()
size := button.Size()
button2 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "World!",
Size: 14,
Color: render.Blue,
}))
button2.SetPoint(render.Point{
X: point.X + size.W,
Y: point.Y,
})
button2.Present(d.Engine)
button.SetText("Buttons that don't click yet")
button.SetPoint(render.NewPoint(250, 300))
button.Label.Text.Size = 24
button.Border = 8
button.Outline = 4
button.Present(d.Engine)
button2.SetText("Multiple colors, too")
button2.Label.Text.Color = render.White
button2.Background = render.RGBA(0, 153, 255, 255)
button2.HighlightColor = render.RGBA(100, 200, 255, 255)
button2.ShadowColor = render.RGBA(0, 100, 153, 255)
button2.SetPoint(render.NewPoint(10, 300))
button2.Present(d.Engine)
_ = label
return nil
}

22
fps.go
View File

@ -10,6 +10,13 @@ import (
// Frames to cache for FPS calculation.
const maxSamples = 100
// Debug mode options, these can be enabled in the dev console
// like: boolProp DebugOverlay true
var (
DebugOverlay = true
DebugCollision = true
)
var (
fpsCurrentTicks uint32 // current time we get sdl.GetTicks()
fpsLastTime uint32 // last time we printed the fpsCurrentTicks
@ -21,7 +28,7 @@ var (
// DrawDebugOverlay draws the debug FPS text on the SDL canvas.
func (d *Doodle) DrawDebugOverlay() {
if !d.Debug {
if !d.Debug || !DebugOverlay {
return
}
@ -29,7 +36,7 @@ func (d *Doodle) DrawDebugOverlay() {
"FPS: %d (%dms) S:%s F12=screenshot",
fpsCurrent,
fpsSkipped,
d.scene.Name(),
d.Scene.Name(),
)
err := d.Engine.DrawText(
@ -52,6 +59,10 @@ func (d *Doodle) DrawDebugOverlay() {
// DrawCollisionBox draws the collision box around a Doodad.
func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) {
if !d.Debug || !DebugCollision {
return
}
var (
rect = doodads.GetBoundingRect(actor)
box = doodads.GetCollisionBox(rect)
@ -74,13 +85,6 @@ func (d *Doodle) TrackFPS(skipped uint32) {
}
if fpsLastTime < fpsCurrentTicks-fpsInterval {
// log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms",
// time.Now().Sub(d.startTime),
// fpsCurrent,
// fpsCurrentTicks-fpsLastTime,
// skipped,
// )
fpsLastTime = fpsCurrentTicks
fpsCurrent = fpsFrames
fpsFrames = 0

74
main_scene.go Normal file
View File

@ -0,0 +1,74 @@
package doodle
import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// MainScene implements the main menu of Doodle.
type MainScene struct {
}
// Name of the scene.
func (s *MainScene) Name() string {
return "Main"
}
// Setup the scene.
func (s *MainScene) Setup(d *Doodle) error {
return nil
}
// Loop the editor scene.
func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
return nil
}
// Draw the pixels on this frame.
func (s *MainScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
label := ui.NewLabel(render.Text{
Text: "Doodle v" + Version,
Size: 26,
Color: render.Pink,
Stroke: render.SkyBlue,
Shadow: render.Black,
})
label.Compute(d.Engine)
label.MoveTo(render.Point{
X: (d.width / 2) - (label.Size().W / 2),
Y: 120,
})
label.Present(d.Engine)
button := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button.Compute(d.Engine)
button.MoveTo(render.Point{
X: (d.width / 2) - (button.Size().W / 2),
Y: 200,
})
button.Present(d.Engine)
button.SetText("Load Map")
button.Compute(d.Engine)
button.MoveTo(render.Point{
X: (d.width / 2) - (button.Size().W / 2),
Y: 260,
})
button.Present(d.Engine)
return nil
}
// Destroy the scene.
func (s *MainScene) Destroy() error {
return nil
}

View File

@ -21,7 +21,7 @@ type PlayScene struct {
height int32
// Player character
player doodads.Doodad
Player doodads.Doodad
}
// Name of the scene.
@ -42,7 +42,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.Filename = ""
}
s.player = doodads.NewPlayer()
s.Player = doodads.NewPlayer()
if s.canvas == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
@ -80,17 +80,17 @@ func (s *PlayScene) Draw(d *Doodle) error {
s.canvas.Draw(d.Engine)
// Draw our hero.
s.player.Draw(d.Engine)
s.Player.Draw(d.Engine)
// Draw out bounding boxes.
d.DrawCollisionBox(s.player)
d.DrawCollisionBox(s.Player)
return nil
}
// movePlayer updates the player's X,Y coordinate based on key pressed.
func (s *PlayScene) movePlayer(ev *events.State) {
delta := s.player.Position()
delta := s.Player.Position()
var playerSpeed int32 = 8
var gravity int32 = 2
@ -110,20 +110,20 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// Apply gravity.
// var onFloor bool
info, ok := doodads.CollidesWithGrid(s.player, &s.canvas, delta)
info, ok := doodads.CollidesWithGrid(s.Player, &s.canvas, delta)
if ok {
// Collision happened with world.
}
delta = info.MoveTo
// Apply gravity if not grounded.
if !s.player.Grounded() {
if !s.Player.Grounded() {
// Gravity has to pipe through the collision checker, too, so it
// can't give us a cheated downward boost.
delta.Y += gravity
}
s.player.MoveTo(delta)
s.Player.MoveTo(delta)
}
// LoadLevel loads a level from disk.

View File

@ -21,11 +21,11 @@ type Scene interface {
// Goto a scene. First it unloads the current scene.
func (d *Doodle) Goto(scene Scene) error {
// Teardown existing scene.
if d.scene != nil {
d.scene.Destroy()
if d.Scene != nil {
d.Scene.Destroy()
}
log.Info("Goto Scene")
d.scene = scene
return d.scene.Setup(d)
log.Info("Goto Scene: %s", scene.Name())
d.Scene = scene
return d.Scene.Setup(d)
}

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"github.com/robertkrimen/otto"
)
// Flash a message to the user.
@ -17,16 +18,26 @@ func (d *Doodle) Flash(template string, v ...interface{}) {
// 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
parent *Doodle
Open bool
Prompt string
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.
@ -37,15 +48,32 @@ type Flash struct {
// NewShell initializes the shell helper (the "Shellper").
func NewShell(d *Doodle) Shell {
return Shell{
s := Shell{
parent: d,
History: []string{},
Output: []string{},
Flashes: []Flash{},
Prompt: ">",
Cursor: "_",
cursor: '_',
cursorRate: balance.ShellCursorBlinkRate,
js: otto.New(),
}
// Make the Doodle instance available to the shell.
bindings := map[string]interface{}{
"d": d,
"log": log,
"RGBA": render.RGBA,
"Point": render.NewPoint,
}
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.
@ -54,11 +82,18 @@ func (s *Shell) Close() {
s.Open = false
s.Prompt = ">"
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)
}
if command.Command == "clear" {
s.Output = []string{}
} else {
@ -68,10 +103,6 @@ func (s *Shell) Execute(input string) {
}
}
if command.Raw != "" {
s.History = append(s.History, command.Raw)
}
// Reset the text buffer in the shell.
s.Text = ""
}
@ -149,6 +180,32 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
s.Execute(s.Text)
s.Close()
return nil
} else if (ev.Up.Now || ev.Down.Now) && 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.
ev.Down.Read()
isUp := ev.Up.Read()
// 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]
}
// Compute the line height we can draw.
@ -159,10 +216,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Cursor flip?
if d.ticks > s.cursorFlip {
s.cursorFlip = d.ticks + s.cursorRate
if s.Cursor == "" {
s.Cursor = "_"
if s.cursor == ' ' {
s.cursor = '_'
} else {
s.Cursor = ""
s.cursor = ' '
}
}
@ -215,7 +272,7 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Draw the command prompt.
d.Engine.DrawText(
render.Text{
Text: s.Prompt + s.Text + s.Cursor,
Text: s.Prompt + s.Text + string(s.cursor),
Size: balance.ShellFontSize,
Color: balance.ShellForegroundColor,
},

View File

@ -90,7 +90,7 @@ func (w *Button) Present(e render.Engine) {
e.DrawBox(w.Background, box)
// Draw the text label inside.
w.Label.SetPoint(render.Point{
w.Label.MoveTo(render.Point{
X: P.X + w.Padding + w.Border + w.Outline,
Y: P.Y + w.Padding + w.Border + w.Outline,
})

View File

@ -9,7 +9,8 @@ type Widget interface {
SetWidth(int32) // Set
SetHeight(int32) // Set
Point() render.Point
SetPoint(render.Point)
MoveTo(render.Point)
MoveBy(render.Point)
Size() render.Rect // Return the Width and Height of the widget.
Resize(render.Rect)
@ -35,11 +36,17 @@ func (w *BaseWidget) Point() render.Point {
return w.point
}
// SetPoint updates the X,Y position of the widget relative to the window.
func (w *BaseWidget) SetPoint(v render.Point) {
// MoveTo updates the X,Y position to the new point.
func (w *BaseWidget) MoveTo(v render.Point) {
w.point = v
}
// MoveBy adds the X,Y values to the widget's current position.
func (w *BaseWidget) MoveBy(v render.Point) {
w.point.X += v.X
w.point.Y += v.Y
}
// Size returns the box with W and H attributes containing the size of the
// widget. The X,Y attributes of the box are ignored and zero.
func (w *BaseWidget) Size() render.Rect {