Add MenuScene with New Level UI

* Debug mode: no longer enables the DebugOverlay (F3) by default, but
  does now insert the current FPS counter into the window title bar.
* ui.Frame: set a default "mostly transparent" BG color so the frame
  background doesn't render as white.
* Add the MenuScene which will house the game's main menus.
* The "New Level" menu is first to be added.
  * UI lets you pick Page Type and Wallpaper using radio buttons.
  * Page Type: Unbounded, Bounded (default), No Negative Space, Bordered
  * Fix bugs in uix.Canvas to fully support all these page types.
This commit is contained in:
Noah 2019-06-25 14:57:11 -07:00
parent 1150d6d3e9
commit 4dd1bebc5f
15 changed files with 403 additions and 21 deletions

View File

@ -22,6 +22,7 @@ type Engine interface {
// Clear the full canvas and set this color. // Clear the full canvas and set this color.
Clear(Color) Clear(Color)
SetTitle(string)
DrawPoint(Color, Point) DrawPoint(Color, Point)
DrawLine(Color, Point, Point) DrawLine(Color, Point, Point)
DrawRect(Color, Rect) DrawRect(Color, Rect)

View File

@ -84,6 +84,12 @@ func (r *Renderer) Setup() error {
return nil return nil
} }
// SetTitle sets the SDL window title.
func (r *Renderer) SetTitle(title string) {
r.title = title
r.window.SetTitle(title)
}
// GetTicks gets SDL's current tick count. // GetTicks gets SDL's current tick count.
func (r *Renderer) GetTicks() uint32 { func (r *Renderer) GetTicks() uint32 {
return sdl.GetTicks() return sdl.GetTicks()

View File

@ -21,6 +21,7 @@ func NewFrame(name string) *Frame {
packs: map[Anchor][]packedWidget{}, packs: map[Anchor][]packedWidget{},
widgets: []Widget{}, widgets: []Widget{},
} }
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
w.IDFunc(func() string { w.IDFunc(func() string {
return fmt.Sprintf("Frame<%s>", return fmt.Sprintf("Frame<%s>",
name, name,

View File

@ -37,6 +37,21 @@ var (
Color: render.Black, Color: render.Black,
} }
// UIFont is the main font for UI labels.
UIFont = render.Text{
Size: 12,
Padding: 4,
Color: render.Black,
}
// LabelFont is the font for strong labels in UI.
LabelFont = render.Text{
Size: 12,
FontFilename: "./fonts/DejaVuSans-Bold.ttf",
Padding: 4,
Color: render.Black,
}
// Color for draggable doodad. // Color for draggable doodad.
DragColor = render.MustHexColor("#0099FF") DragColor = render.MustHexColor("#0099FF")
) )

View File

@ -93,8 +93,7 @@ func (c Command) Run(d *Doodle) error {
// New opens a new map in the editor mode. // New opens a new map in the editor mode.
func (c Command) New(d *Doodle) error { func (c Command) New(d *Doodle) error {
d.Flash("Starting a new map") d.GotoNewMenu()
d.NewMap()
return nil return nil
} }

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"github.com/kirsle/golog" "github.com/kirsle/golog"
@ -57,12 +58,17 @@ func New(debug bool, engine render.Engine) *Doodle {
if debug { if debug {
log.Logger.Config.Level = golog.DebugLevel log.Logger.Config.Level = golog.DebugLevel
DebugOverlay = true // on by default in debug mode, F3 to disable // DebugOverlay = true // on by default in debug mode, F3 to disable
} }
return d return d
} }
// Title returns the game's preferred window title.
func (d *Doodle) Title() string {
return fmt.Sprintf("%s v%s", branding.AppName, branding.Version)
}
// SetupEngine sets up the rendering engine. // SetupEngine sets up the rendering engine.
func (d *Doodle) SetupEngine() error { func (d *Doodle) SetupEngine() error {
if err := d.Engine.Setup(); err != nil { if err := d.Engine.Setup(); err != nil {
@ -83,8 +89,8 @@ func (d *Doodle) Run() error {
// Set up the default scene. // Set up the default scene.
if d.Scene == nil { if d.Scene == nil {
// d.Goto(&GUITestScene{}) // d.Goto(&GUITestScene{})
d.NewMap() // d.NewMap()
// d.Goto(&MainScene{}) d.Goto(&MainScene{})
} }
log.Info("Enter Main Loop") log.Info("Enter Main Loop")

View File

@ -363,7 +363,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
menuButton{ menuButton{
Text: "New Level", Text: "New Level",
Click: func(render.Point) { Click: func(render.Point) {
d.NewMap() d.GotoNewMenu()
}, },
}, },
menuButton{ menuButton{

View File

@ -153,6 +153,8 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
} }
// TrackFPS shows the current FPS once per second. // TrackFPS shows the current FPS once per second.
//
// In debug mode, changes the window title to include the FPS counter.
func (d *Doodle) TrackFPS(skipped uint32) { func (d *Doodle) TrackFPS(skipped uint32) {
fpsFrames++ fpsFrames++
fpsCurrentTicks = d.Engine.GetTicks() fpsCurrentTicks = d.Engine.GetTicks()
@ -168,4 +170,8 @@ func (d *Doodle) TrackFPS(skipped uint32) {
fpsFrames = 0 fpsFrames = 0
fpsSkipped = skipped fpsSkipped = skipped
} }
if d.Debug {
d.Engine.SetTitle(fmt.Sprintf("%s (%d FPS)", d.Title(), fpsCurrent))
}
} }

View File

@ -1,5 +1,7 @@
package level package level
import "fmt"
// PageType configures the bounds and wallpaper behavior of a Level. // PageType configures the bounds and wallpaper behavior of a Level.
type PageType int type PageType int
@ -29,4 +31,32 @@ const (
// - The wallpaper vert mirrors Top along the Y=Width plane // - The wallpaper vert mirrors Top along the Y=Width plane
// - The wallpaper 180 rotates the Corner for opposite corners // - The wallpaper 180 rotates the Corner for opposite corners
Bordered Bordered
// If you add new PageType, also update the two functions below.
) )
// String converts the PageType to a string label.
func (p PageType) String() string {
switch p {
case Unbounded:
return "Unbounded"
case NoNegativeSpace:
return "NoNegativeSpace"
case Bounded:
return "Bounded"
case Bordered:
return "Bordered"
}
return fmt.Sprintf("PageType<%d>", p)
}
// PageTypeFromString returns a PageType from its string version.
func PageTypeFromString(name string) (PageType, bool) {
// The min and max PageType value.
for i := Unbounded; i <= Bordered; i++ {
if name == i.String() {
return PageType(i), true
}
}
return 0, false
}

View File

@ -6,12 +6,18 @@ import (
"git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/uix"
) )
// MainScene implements the main menu of Doodle. // MainScene implements the main menu of Doodle.
type MainScene struct { type MainScene struct {
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
frame *ui.Frame frame *ui.Frame
// Background wallpaper canvas.
canvas *uix.Canvas
} }
// Name of the scene. // Name of the scene.
@ -23,6 +29,20 @@ func (s *MainScene) Name() string {
func (s *MainScene) Setup(d *Doodle) error { func (s *MainScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor() s.Supervisor = ui.NewSupervisor()
// Set up the background wallpaper canvas.
s.canvas = uix.NewCanvas(100, false)
s.canvas.Resize(render.Rect{
W: int32(d.width),
H: int32(d.height),
})
s.canvas.LoadLevel(d.Engine, &level.Level{
Chunker: level.NewChunker(100),
Palette: level.NewPalette(),
PageType: level.Bounded,
Wallpaper: "notebook.png",
})
// Main UI button frame.
frame := ui.NewFrame("frame") frame := ui.NewFrame("frame")
s.frame = frame s.frame = frame
@ -31,14 +51,13 @@ func (s *MainScene) Setup(d *Doodle) error {
Font: balance.StatusFont, Font: balance.StatusFont,
})) }))
button1.Handle(ui.Click, func(p render.Point) { button1.Handle(ui.Click, func(p render.Point) {
d.NewMap() d.GotoNewMenu()
}) })
button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{ button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{
Text: "New Map", Text: "Load Map",
Font: balance.StatusFont, Font: balance.StatusFont,
})) }))
button2.SetText("Load Map")
frame.Pack(button1, ui.Pack{ frame.Pack(button1, ui.Pack{
Anchor: ui.N, Anchor: ui.N,
@ -59,6 +78,18 @@ func (s *MainScene) Setup(d *Doodle) error {
// Loop the editor scene. // Loop the editor scene.
func (s *MainScene) Loop(d *Doodle, ev *events.State) error { func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
s.Supervisor.Loop(ev) s.Supervisor.Loop(ev)
if resized := ev.Resized.Read(); resized {
w, h := d.Engine.WindowSize()
d.width = w
d.height = h
log.Info("Resized to %dx%d", d.width, d.height)
s.canvas.Resize(render.Rect{
W: int32(d.width),
H: int32(d.height),
})
}
return nil return nil
} }
@ -67,6 +98,8 @@ func (s *MainScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white. // Clear the canvas and fill it with white.
d.Engine.Clear(render.White) d.Engine.Clear(render.White)
s.canvas.Present(d.Engine, render.Origin)
label := ui.NewLabel(ui.Label{ label := ui.NewLabel(ui.Label{
Text: branding.AppName, Text: branding.AppName,
Font: render.Text{ Font: render.Text{

282
pkg/menu_scene.go Normal file
View File

@ -0,0 +1,282 @@
package doodle
import (
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
)
/*
MenuScene holds the main dialog menu UIs for:
* New Level
* Open Level
* Settings
*/
type MenuScene struct {
// Configuration.
StartupMenu string
Supervisor *ui.Supervisor
// Private widgets.
window *ui.Window
// Values for the New menu
newPageType string
newWallpaper string
}
// Name of the scene.
func (s *MenuScene) Name() string {
return "Menu"
}
// GotoNewMenu loads the MenuScene and shows the "New" window.
func (d *Doodle) GotoNewMenu() {
log.Info("Loading the MenuScene to the New window")
scene := &MenuScene{
StartupMenu: "new",
}
d.Goto(scene)
}
// GotoLoadMenu loads the MenuScene and shows the "Load" window.
func (d *Doodle) GotoLoadMenu() {
log.Info("Loading the MenuScene to the Load window")
scene := &MenuScene{
StartupMenu: "load",
}
d.Goto(scene)
}
// Setup the scene.
func (s *MenuScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor()
switch s.StartupMenu {
case "new":
if err := s.setupNewWindow(d); err != nil {
return err
}
case "load":
if err := s.setupLoadWindow(d); err != nil {
return err
}
default:
d.Flash("No Valid StartupMenu Given to MenuScene")
}
return nil
}
// setupNewWindow sets up the UI for the "New" window.
func (s *MenuScene) setupNewWindow(d *Doodle) error {
// Default scene options.
s.newPageType = level.Bounded.String()
s.newWallpaper = "notebook.png"
window := ui.NewWindow("New Drawing")
window.Configure(ui.Config{
Width: int32(float64(d.width) * 0.8),
Height: int32(float64(d.height) * 0.8),
Background: render.Grey,
})
window.Compute(d.Engine)
{
frame := ui.NewFrame("New Level Frame")
window.Pack(frame, ui.Pack{
Anchor: ui.N,
Fill: true,
Expand: true,
})
/******************
* Frame for selecting Page Type
******************/
label1 := ui.NewLabel(ui.Label{
Text: "Page Type",
Font: balance.LabelFont,
})
frame.Pack(label1, ui.Pack{
Anchor: ui.N,
FillX: true,
})
typeFrame := ui.NewFrame("Page Type Options Frame")
frame.Pack(typeFrame, ui.Pack{
Anchor: ui.N,
FillX: true,
})
var types = []struct {
Name string
Value level.PageType
}{
{"Unbounded", level.Unbounded},
{"Bounded", level.Bounded},
{"No Negative Space", level.NoNegativeSpace},
{"Bordered", level.Bordered},
}
for _, t := range types {
// Hide some options for the free version of the game.
if balance.FreeVersion {
if t.Value != level.Bounded {
continue
}
}
radio := ui.NewRadioButton(t.Name,
&s.newPageType,
t.Value.String(),
ui.NewLabel(ui.Label{
Text: t.Name,
Font: balance.MenuFont,
}),
)
s.Supervisor.Add(radio)
typeFrame.Pack(radio, ui.Pack{
Anchor: ui.W,
PadX: 4,
})
}
/******************
* Frame for selecting Level Wallpaper
******************/
label2 := ui.NewLabel(ui.Label{
Text: "Wallpaper",
Font: balance.LabelFont,
})
frame.Pack(label2, ui.Pack{
Anchor: ui.N,
FillX: true,
})
wpFrame := ui.NewFrame("Wallpaper Frame")
frame.Pack(wpFrame, ui.Pack{
Anchor: ui.N,
FillX: true,
})
var wallpapers = []struct {
Name string
Value string
}{
{"Notebook", "notebook.png"},
{"Blueprint", "blueprint.png"},
{"Legal Pad", "legal.png"},
{"Placemat", "placemat.png"},
}
for _, t := range wallpapers {
radio := ui.NewRadioButton(t.Name, &s.newWallpaper, t.Value, ui.NewLabel(ui.Label{
Text: t.Name,
Font: balance.MenuFont,
}))
s.Supervisor.Add(radio)
wpFrame.Pack(radio, ui.Pack{
Anchor: ui.W,
PadX: 4,
})
}
/******************
* Confirm/cancel buttons.
******************/
bottomFrame := ui.NewFrame("Button Frame")
// bottomFrame.Configure(ui.Config{
// BorderSize: 1,
// BorderStyle: ui.BorderSunken,
// BorderColor: render.Black,
// })
// bottomFrame.SetBackground(render.Grey)
frame.Pack(bottomFrame, ui.Pack{
Anchor: ui.N,
FillX: true,
PadY: 8,
})
var buttons = []struct {
Label string
F func(render.Point)
}{
{"Continue", func(p render.Point) {
d.Flash("Create new map with %s page type and %s wallpaper", s.newPageType, s.newWallpaper)
pageType, ok := level.PageTypeFromString(s.newPageType)
if !ok {
d.Flash("Invalid Page Type '%s'", s.newPageType)
return
}
lvl := level.New()
lvl.Palette = level.DefaultPalette()
lvl.Wallpaper = s.newWallpaper
lvl.PageType = pageType
d.Goto(&EditorScene{
DrawingType: enum.LevelDrawing,
Level: lvl,
})
}},
{"Cancel", func(p render.Point) {
d.Goto(&MainScene{})
}},
}
for _, t := range buttons {
btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{
Text: t.Label,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, t.F)
s.Supervisor.Add(btn)
bottomFrame.Pack(btn, ui.Pack{
Anchor: ui.W,
PadX: 4,
PadY: 8,
})
}
}
s.window = window
return nil
}
// setupLoadWindow sets up the UI for the "New" window.
func (s *MenuScene) setupLoadWindow(d *Doodle) error {
return nil
}
// Loop the editor scene.
func (s *MenuScene) Loop(d *Doodle, ev *events.State) error {
s.Supervisor.Loop(ev)
return nil
}
// Draw the pixels on this frame.
func (s *MenuScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
s.window.Compute(d.Engine)
s.window.MoveTo(render.Point{
X: (int32(d.width) / 2) - (s.window.Size().W / 2),
Y: 60,
})
s.window.Present(d.Engine, s.window.Point())
return nil
}
// Destroy the scene.
func (s *MenuScene) Destroy() error {
return nil
}

View File

@ -123,7 +123,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
*s.debScroll = s.drawing.Scroll.String() *s.debScroll = s.drawing.Scroll.String()
// Has the window been resized? // Has the window been resized?
if resized := ev.Resized.Read(); resized { if resized := ev.Resized.Now; resized {
w, h := d.Engine.WindowSize() w, h := d.Engine.WindowSize()
if w != d.width || h != d.height { if w != d.width || h != d.height {
d.width = w d.width = w

View File

@ -8,8 +8,8 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/collision"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
"github.com/kirsle/blog/src/log"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
) )

View File

@ -69,7 +69,8 @@ func (w *Canvas) loopConstrainScroll() error {
} }
// Constrain the bottom and right for limited world sizes. // Constrain the bottom and right for limited world sizes.
if w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 { if w.wallpaper.pageType >= level.Bounded &&
w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 {
var ( var (
// TODO: downcast from int64! // TODO: downcast from int64!
mw = int32(w.wallpaper.maxWidth) mw = int32(w.wallpaper.maxWidth)

View File

@ -46,16 +46,18 @@ func (w *Canvas) loopContainActorsInsideLevel(a *Actor) {
} }
// Bound it on the right bottom edges. XXX: downcast from int64! // Bound it on the right bottom edges. XXX: downcast from int64!
if w.wallpaper.maxWidth > 0 { if w.wallpaper.pageType >= level.Bounded {
if int64(orig.X+size.W) > w.wallpaper.maxWidth { if w.wallpaper.maxWidth > 0 {
var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W)) if int64(orig.X+size.W) > w.wallpaper.maxWidth {
moveBy.X = delta var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W))
moveBy.X = delta
}
} }
} if w.wallpaper.maxHeight > 0 {
if w.wallpaper.maxHeight > 0 { if int64(orig.Y+size.H) > w.wallpaper.maxHeight {
if int64(orig.Y+size.H) > w.wallpaper.maxHeight { var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H))
var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H)) moveBy.Y = delta
moveBy.Y = delta }
} }
} }