From 4dd1bebc5fcd5989d9d453259f096cb4aa295c51 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 25 Jun 2019 14:57:11 -0700 Subject: [PATCH] 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. --- lib/render/interface.go | 1 + lib/render/sdl/sdl.go | 6 + lib/ui/frame.go | 1 + pkg/balance/theme.go | 15 ++ pkg/commands.go | 3 +- pkg/doodle.go | 12 +- pkg/editor_ui.go | 2 +- pkg/fps.go | 6 + pkg/level/page_type.go | 30 ++++ pkg/main_scene.go | 39 ++++- pkg/menu_scene.go | 282 ++++++++++++++++++++++++++++++++++++ pkg/play_scene.go | 2 +- pkg/uix/actor_collision.go | 2 +- pkg/uix/canvas_scrolling.go | 3 +- pkg/uix/canvas_wallpaper.go | 20 +-- 15 files changed, 403 insertions(+), 21 deletions(-) create mode 100644 pkg/menu_scene.go diff --git a/lib/render/interface.go b/lib/render/interface.go index 092ff08..2c265a5 100644 --- a/lib/render/interface.go +++ b/lib/render/interface.go @@ -22,6 +22,7 @@ type Engine interface { // Clear the full canvas and set this color. Clear(Color) + SetTitle(string) DrawPoint(Color, Point) DrawLine(Color, Point, Point) DrawRect(Color, Rect) diff --git a/lib/render/sdl/sdl.go b/lib/render/sdl/sdl.go index 9bdd1e7..a3a176a 100644 --- a/lib/render/sdl/sdl.go +++ b/lib/render/sdl/sdl.go @@ -84,6 +84,12 @@ func (r *Renderer) Setup() error { 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. func (r *Renderer) GetTicks() uint32 { return sdl.GetTicks() diff --git a/lib/ui/frame.go b/lib/ui/frame.go index fe3be40..e438f23 100644 --- a/lib/ui/frame.go +++ b/lib/ui/frame.go @@ -21,6 +21,7 @@ func NewFrame(name string) *Frame { packs: map[Anchor][]packedWidget{}, widgets: []Widget{}, } + w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG w.IDFunc(func() string { return fmt.Sprintf("Frame<%s>", name, diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 7da0987..17221e4 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -37,6 +37,21 @@ var ( 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. DragColor = render.MustHexColor("#0099FF") ) diff --git a/pkg/commands.go b/pkg/commands.go index 8eef50a..8ee8f5e 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -93,8 +93,7 @@ func (c Command) Run(d *Doodle) error { // New opens a new map in the editor mode. func (c Command) New(d *Doodle) error { - d.Flash("Starting a new map") - d.NewMap() + d.GotoNewMenu() return nil } diff --git a/pkg/doodle.go b/pkg/doodle.go index a64425a..5250fee 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/render" "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/log" "github.com/kirsle/golog" @@ -57,12 +58,17 @@ func New(debug bool, engine render.Engine) *Doodle { if debug { 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 } +// 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. func (d *Doodle) SetupEngine() error { if err := d.Engine.Setup(); err != nil { @@ -83,8 +89,8 @@ func (d *Doodle) Run() error { // Set up the default scene. if d.Scene == nil { // d.Goto(&GUITestScene{}) - d.NewMap() - // d.Goto(&MainScene{}) + // d.NewMap() + d.Goto(&MainScene{}) } log.Info("Enter Main Loop") diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index b8767a2..ae35f2a 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -363,7 +363,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { menuButton{ Text: "New Level", Click: func(render.Point) { - d.NewMap() + d.GotoNewMenu() }, }, menuButton{ diff --git a/pkg/fps.go b/pkg/fps.go index e74125a..3179c38 100644 --- a/pkg/fps.go +++ b/pkg/fps.go @@ -153,6 +153,8 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) { } // 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) { fpsFrames++ fpsCurrentTicks = d.Engine.GetTicks() @@ -168,4 +170,8 @@ func (d *Doodle) TrackFPS(skipped uint32) { fpsFrames = 0 fpsSkipped = skipped } + + if d.Debug { + d.Engine.SetTitle(fmt.Sprintf("%s (%d FPS)", d.Title(), fpsCurrent)) + } } diff --git a/pkg/level/page_type.go b/pkg/level/page_type.go index 668c0a6..c975efa 100644 --- a/pkg/level/page_type.go +++ b/pkg/level/page_type.go @@ -1,5 +1,7 @@ package level +import "fmt" + // PageType configures the bounds and wallpaper behavior of a Level. type PageType int @@ -29,4 +31,32 @@ const ( // - The wallpaper vert mirrors Top along the Y=Width plane // - The wallpaper 180 rotates the Corner for opposite corners 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 +} diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 81840ae..57a646b 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -6,12 +6,18 @@ import ( "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" "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. type MainScene struct { Supervisor *ui.Supervisor frame *ui.Frame + + // Background wallpaper canvas. + canvas *uix.Canvas } // Name of the scene. @@ -23,6 +29,20 @@ func (s *MainScene) Name() string { func (s *MainScene) Setup(d *Doodle) error { 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") s.frame = frame @@ -31,14 +51,13 @@ func (s *MainScene) Setup(d *Doodle) error { Font: balance.StatusFont, })) button1.Handle(ui.Click, func(p render.Point) { - d.NewMap() + d.GotoNewMenu() }) button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{ - Text: "New Map", + Text: "Load Map", Font: balance.StatusFont, })) - button2.SetText("Load Map") frame.Pack(button1, ui.Pack{ Anchor: ui.N, @@ -59,6 +78,18 @@ func (s *MainScene) Setup(d *Doodle) error { // Loop the editor scene. func (s *MainScene) Loop(d *Doodle, ev *events.State) error { 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 } @@ -67,6 +98,8 @@ func (s *MainScene) Draw(d *Doodle) error { // Clear the canvas and fill it with white. d.Engine.Clear(render.White) + s.canvas.Present(d.Engine, render.Origin) + label := ui.NewLabel(ui.Label{ Text: branding.AppName, Font: render.Text{ diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go new file mode 100644 index 0000000..1167343 --- /dev/null +++ b/pkg/menu_scene.go @@ -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 +} diff --git a/pkg/play_scene.go b/pkg/play_scene.go index c2ae7ab..abaed60 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -123,7 +123,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { *s.debScroll = s.drawing.Scroll.String() // Has the window been resized? - if resized := ev.Resized.Read(); resized { + if resized := ev.Resized.Now; resized { w, h := d.Engine.WindowSize() if w != d.width || h != d.height { d.width = w diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 99034cb..47bae67 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -8,8 +8,8 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/scripting" - "github.com/kirsle/blog/src/log" "github.com/robertkrimen/otto" ) diff --git a/pkg/uix/canvas_scrolling.go b/pkg/uix/canvas_scrolling.go index 8dffdf4..c42eaab 100644 --- a/pkg/uix/canvas_scrolling.go +++ b/pkg/uix/canvas_scrolling.go @@ -69,7 +69,8 @@ func (w *Canvas) loopConstrainScroll() error { } // 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 ( // TODO: downcast from int64! mw = int32(w.wallpaper.maxWidth) diff --git a/pkg/uix/canvas_wallpaper.go b/pkg/uix/canvas_wallpaper.go index 8c5f25d..86bc1ac 100644 --- a/pkg/uix/canvas_wallpaper.go +++ b/pkg/uix/canvas_wallpaper.go @@ -46,16 +46,18 @@ func (w *Canvas) loopContainActorsInsideLevel(a *Actor) { } // Bound it on the right bottom edges. XXX: downcast from int64! - if w.wallpaper.maxWidth > 0 { - if int64(orig.X+size.W) > w.wallpaper.maxWidth { - var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W)) - moveBy.X = delta + if w.wallpaper.pageType >= level.Bounded { + if w.wallpaper.maxWidth > 0 { + if int64(orig.X+size.W) > w.wallpaper.maxWidth { + var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W)) + moveBy.X = delta + } } - } - if w.wallpaper.maxHeight > 0 { - if int64(orig.Y+size.H) > w.wallpaper.maxHeight { - var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H)) - moveBy.Y = delta + if w.wallpaper.maxHeight > 0 { + if int64(orig.Y+size.H) > w.wallpaper.maxHeight { + var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H)) + moveBy.Y = delta + } } }