diff --git a/Ideas.md b/Ideas.md index 3e4ca85..79c1b23 100644 --- a/Ideas.md +++ b/Ideas.md @@ -222,11 +222,14 @@ Probably mostly DRM free. Will want some sort of account server early-on though. * The texture file will be a square (rectangular maybe ok) with four quadrants from which the textures will be extracted. For example if the overall image size was 100x100 pixels, it will be divided into the four 50x50 quadrants. - 1. `TL`: Top left corner is the top left edge of the "page" the level is on - 2. `TR`: Top right corner is the repeated "top of page" texture. - 3. `BL`: Bottom left corner is the repeated "left of page" texture. - 4. `BR`: Bottom right corner is the repeated background texture that extends + 1. `Corner`: Top left corner is the top left edge of the "page" the level is on + 2. `Top`: Top right corner is the repeated "top of page" texture. + 3. `Left`: Bottom left corner is the repeated "left of page" texture. + 4. `Repeat`: Bottom right corner is the repeated background texture that extends infinitely in all directions. +* The Repeat texture is used all the time, and the other three are used when the + level type has boundaries (on the top and left edges in particular) to draw + decorative borders instead of the Repeat texture. * Levels will be able to choose a "page type" which controls how the wallpaper will be drawn and how the level boundaries may be constrained. There will be four options: @@ -241,22 +244,14 @@ Probably mostly DRM free. Will want some sort of account server early-on though. wall. 3. **Bounded:** The map has a fixed width and height and is bounded on all four edges. - 4. **Bounded, Mirrored Wallpaper:** same as Bounded but with a different - wallpaper behavior. -* The page types will have their own behaviors with how wallpapers are drawn: - * **Unbounded:** only the `BR` texture from the wallpaper is used, repeated - infinitely in the X and Y directions. The top-left, top, and left edge - textures are not used. - * **No Negative Space:** the `TL` texture is drawn at coordinate `(0,0)`. - To its right, the `TR` texture is repeated forever in the X direction, and - along the left edge of the page, the `BL` texture is repeated in the Y - direction. The remaining whitespace on the page repeats the `BR` texture - infinitely. - * **Bounded:** same as No Negative Space. - * **Bounded, Mirrored Wallpaper:** same as No Negative Space, but all of the - _other_ corners and edges are textured too, with mirror images of the Top, - Top Left, and Left textures. This would look silly on the "ruled notebook" - texture, but could be useful to emborder the level with a fancy texture. + 4. **Bordered:** same as Bounded but the right and bottom edges are decorated + using mirrors of the top and left textures. This would look silly on notebook + paper (having an extra red line and blank margin) but would work for + wrap-around textures like paper placemats. + 5. **Full Page:** special level type that treats the wallpaper image as a + literal full page scan to be drawn on top of. The level is bounded by the + literal dimensions of the wallpaper image which is drawn once instead of + cut up and tiled as per the usual behavior. * The game will come with a few built-in textures for levels to refer to by name. These textures don't need to be distributed with the map files themselves, as every copy of the game should include these (or a sensible fallback would @@ -264,6 +259,24 @@ Probably mostly DRM free. Will want some sort of account server early-on though. * The map author can also attach their own custom texture that will be included inside the map file. +### Default Wallpapers + +**notebook**: standard ruled notebook paper with a red line alone the Left +dge and a blank margin along the Top, with a Corner and the blue lines +aking up the Repeat in all directions. + +![notebook.png](../assets/wallpapers/notebook.png) + +**graph**: graph paper made up of a grid of light grey or blue lines. + +**dots**: graph paper made of dots at the intersections but not the lines in +between. + +**legal**: yellow lined notebook paper (legal pad). + +**placemat**: a placemat texture with a wavy outline that emborders the map +on all four sides. To be used with the Bordered level type. + # Text Console * Create a rudimentary dev console for entering text commands in-game. It diff --git a/Makefile b/Makefile index 2f08226..b17e095 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,11 @@ SHELL := /bin/bash VERSION=$(shell grep -e 'Version' doodle.go | head -n 1 | cut -d '"' -f 2) BUILD=$(shell git describe --always) +BUILD_DATE=$(shell date -Iseconds) CURDIR=$(shell curdir) # Inject the build version (commit hash) into the executable. -LDFLAGS := -ldflags "-X main.Build=$(BUILD)" +LDFLAGS := -ldflags "-X main.Build=$(BUILD) -X main.BuildDate=$(BUILD_DATE)" # `make setup` to set up a new environment, pull dependencies, etc. .PHONY: setup diff --git a/assets/wallpapers/test-128x128.png b/assets/wallpapers/test-128x128.png new file mode 100644 index 0000000..736302d Binary files /dev/null and b/assets/wallpapers/test-128x128.png differ diff --git a/cmd/doodad/commands/convert.go b/cmd/doodad/commands/convert.go index 2ebfba3..e078d83 100644 --- a/cmd/doodad/commands/convert.go +++ b/cmd/doodad/commands/convert.go @@ -11,10 +11,10 @@ import ( "image/png" - "git.kirsle.net/apps/doodle" - "git.kirsle.net/apps/doodle/doodads" - "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/level" "github.com/urfave/cli" "golang.org/x/image/bmp" ) diff --git a/cmd/doodad/main.go b/cmd/doodad/main.go index b976f43..656fcf2 100644 --- a/cmd/doodad/main.go +++ b/cmd/doodad/main.go @@ -2,22 +2,38 @@ package main import ( + "fmt" "log" "os" "sort" + "time" - "git.kirsle.net/apps/doodle" "git.kirsle.net/apps/doodle/cmd/doodad/commands" + "git.kirsle.net/apps/doodle/src" "github.com/urfave/cli" ) -var Build = "N/A" +// Build number is the git commit hash. +var ( + Build = "" + BuildDate string +) + +func init() { + if BuildDate == "" { + BuildDate = time.Now().Format(time.RFC3339) + } +} func main() { app := cli.NewApp() app.Name = "doodad" app.Usage = "command line interface for Doodle" - app.Version = doodle.Version + " build " + Build + app.Version = fmt.Sprintf("%s build %s. Built on %s", + doodle.Version, + Build, + BuildDate, + ) app.Flags = []cli.Flag{ cli.BoolFlag{ diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 531f91f..aceb8ce 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -1,59 +1,101 @@ package main import ( - "flag" + "fmt" + "log" + "os" "runtime" + "sort" + "time" - "git.kirsle.net/apps/doodle" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/render/sdl" + "git.kirsle.net/apps/doodle/lib/render/sdl" + "git.kirsle.net/apps/doodle/src" + "git.kirsle.net/apps/doodle/src/balance" + "github.com/urfave/cli" _ "image/png" ) // Build number is the git commit hash. -var Build string - -// Command line args var ( - debug bool - edit bool - guitest bool + Build = "" + BuildDate string ) func init() { - flag.BoolVar(&debug, "debug", false, "Debug mode") - flag.BoolVar(&edit, "edit", false, "Edit the map given on the command line. Default is to play the map.") - flag.BoolVar(&guitest, "guitest", false, "Enter the GUI Test scene.") + if BuildDate == "" { + BuildDate = time.Now().Format(time.RFC3339) + } } func main() { runtime.LockOSThread() - flag.Parse() - args := flag.Args() - var filename string - if len(args) > 0 { - filename = args[0] - } - - // SDL engine. - engine := sdl.New( - "Doodle v"+doodle.Version, - balance.Width, - balance.Height, + app := cli.NewApp() + app.Name = "doodle" + app.Usage = "command line interface for Doodle" + app.Version = fmt.Sprintf("%s build %s. Built on %s", + doodle.Version, + Build, + BuildDate, ) - app := doodle.New(debug, engine) - app.SetupEngine() - if guitest { - app.Goto(&doodle.GUITestScene{}) - } else if filename != "" { - if edit { - app.EditFile(filename) - } else { - app.PlayLevel(filename) - } + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "debug, d", + Usage: "enable debug level logging", + }, + cli.BoolFlag{ + Name: "edit, e", + Usage: "edit the map given on the command line (instead of play it)", + }, + cli.BoolFlag{ + Name: "guitest", + Usage: "enter the GUI Test scene on startup", + }, + } + + app.Action = func(c *cli.Context) error { + if c.Bool("version") { + fmt.Printf("This is Project Doodle, v%s build %s ", + doodle.Version, + Build, + ) + return nil + } + + var filename string + if c.NArg() > 0 { + filename = c.Args().Get(0) + } + + // SDL engine. + engine := sdl.New( + "Doodle v"+doodle.Version, + balance.Width, + balance.Height, + ) + + game := doodle.New(c.Bool("debug"), engine) + game.SetupEngine() + if c.Bool("guitest") { + game.Goto(&doodle.GUITestScene{}) + } else if filename != "" { + if c.Bool("edit") { + game.EditFile(filename) + } else { + game.PlayLevel(filename) + } + } + game.Run() + return nil + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) } - app.Run() } diff --git a/doodads/player.go b/doodads/player.go deleted file mode 100644 index 0b63c1c..0000000 --- a/doodads/player.go +++ /dev/null @@ -1,81 +0,0 @@ -package doodads - -import ( - "git.kirsle.net/apps/doodle/render" -) - -// PlayerID is the Doodad ID for the player character. -const PlayerID = "PLAYER" - -// Player is a special doodad for the player character. -type Player struct { - point render.Point - velocity render.Point - size render.Rect - grounded bool -} - -// NewPlayer creates the special Player Character doodad. -func NewPlayer() *Player { - return &Player{ - point: render.Point{ - X: 100, - Y: 100, - }, - size: render.Rect{ - W: 32, - H: 32, - }, - } -} - -// ID of the Player singleton. -func (p *Player) ID() string { - return PlayerID -} - -// Position of the player. -func (p *Player) Position() render.Point { - return p.point -} - -// MoveBy a relative delta position. -func (p *Player) MoveBy(by render.Point) { - p.point.X += by.X - p.point.Y += by.Y -} - -// MoveTo an absolute position. -func (p *Player) MoveTo(to render.Point) { - p.point = to -} - -// Velocity returns the player's current velocity. -func (p *Player) Velocity() render.Point { - return p.velocity -} - -// Size returns the player's size. -func (p *Player) Size() render.Rect { - return p.size -} - -// Grounded returns if the player is grounded. -func (p *Player) Grounded() bool { - return p.grounded -} - -// SetGrounded sets if the player is grounded. -func (p *Player) SetGrounded(v bool) { - p.grounded = v -} - -// Draw the player sprite. -func (p *Player) Draw(e render.Engine) { - e.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{ - X: p.point.X, - Y: p.point.Y, - W: p.size.W, - H: p.size.H, - }) -} diff --git a/render/color.go b/lib/render/color.go similarity index 88% rename from render/color.go rename to lib/render/color.go index c16cda2..f953e49 100644 --- a/render/color.go +++ b/lib/render/color.go @@ -156,15 +156,15 @@ func (c *Color) UnmarshalJSON(b []byte) error { } // Add a relative color value to the color. -func (c Color) Add(r, g, b, a int32) Color { +func (c Color) Add(r, g, b, a int) Color { var ( - R = int32(c.Red) + r - G = int32(c.Green) + g - B = int32(c.Blue) + b - A = int32(c.Alpha) + a + R = int(c.Red) + r + G = int(c.Green) + g + B = int(c.Blue) + b + A = int(c.Alpha) + a ) - cap8 := func(v int32) uint8 { + cap8 := func(v int) uint8 { if v > 255 { v = 255 } else if v < 0 { @@ -182,11 +182,22 @@ func (c Color) Add(r, g, b, a int32) Color { } // Lighten a color value. -func (c Color) Lighten(v int32) Color { +func (c Color) Lighten(v int) Color { return c.Add(v, v, v, 0) } // Darken a color value. -func (c Color) Darken(v int32) Color { +func (c Color) Darken(v int) Color { return c.Add(-v, -v, -v, 0) } + +// Transparentize adjusts the alpha level of a color. +func (c Color) Transparentize(v int) Color { + return c.Add(0, 0, 0, int(v)) +} + +// SetAlpha sets the alpha value to a specific setting. +func (c Color) SetAlpha(v uint8) Color { + c.Alpha = v + return c +} diff --git a/render/functions.go b/lib/render/functions.go similarity index 100% rename from render/functions.go rename to lib/render/functions.go diff --git a/render/interface.go b/lib/render/interface.go similarity index 92% rename from render/interface.go rename to lib/render/interface.go index 1a30ad9..5d307fc 100644 --- a/render/interface.go +++ b/lib/render/interface.go @@ -4,7 +4,7 @@ import ( "fmt" "math" - "git.kirsle.net/apps/doodle/events" + "git.kirsle.net/apps/doodle/src/events" ) // Engine is the interface for the rendering engine, keeping SDL-specific stuff @@ -125,7 +125,8 @@ func (r Rect) Add(other Rect) Rect { } } -// Add a point to move the rect. +// Add (or subtract) a point to move the rect. This is usually the method you +// want to use: negative Point values will subtract the rect's position. func (r Rect) AddPoint(other Point) Rect { return Rect{ X: r.X + other.X, @@ -135,6 +136,17 @@ func (r Rect) AddPoint(other Point) Rect { } } +// SubtractPoint is the inverse of AddPoint. Use this only if you need to +// invert the Point being added. +func (r Rect) SubtractPoint(other Point) Rect { + return Rect{ + X: r.X - other.X, + Y: r.Y - other.Y, + W: r.W, + H: r.H, + } +} + // Text holds information for drawing text. type Text struct { Text string diff --git a/render/log.go b/lib/render/log.go similarity index 100% rename from render/log.go rename to lib/render/log.go diff --git a/render/point.go b/lib/render/point.go similarity index 83% rename from render/point.go rename to lib/render/point.go index 272238c..49f8576 100644 --- a/render/point.go +++ b/lib/render/point.go @@ -69,12 +69,21 @@ func (p Point) Inside(r Rect) bool { (p.Y >= y1 && p.Y <= y2)) } -// Add (or subtract) the other point to your current point. +// Add (or subtract) the other point to your current point. This is usually +// the one you want: if the other Point has negative values it will subtract +// them from this Point, or if they are positive it will add them. func (p *Point) Add(other Point) { p.X += other.X p.Y += other.Y } +// Subtract is the inverse of Add. Use this if you want to force a subtraction +// operation (i.e. to invert a Point before adding it). +func (p *Point) Subtract(other Point) { + p.X -= other.X + p.Y -= other.Y +} + // MarshalText to convert the point into text so that a render.Point may be used // as a map key and serialized to JSON. func (p *Point) MarshalText() ([]byte, error) { diff --git a/render/point_test.go b/lib/render/point_test.go similarity index 95% rename from render/point_test.go rename to lib/render/point_test.go index d56a70a..0cfe102 100644 --- a/render/point_test.go +++ b/lib/render/point_test.go @@ -4,7 +4,7 @@ import ( "strconv" "testing" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) func TestPointInside(t *testing.T) { diff --git a/render/rect_test.go b/lib/render/rect_test.go similarity index 96% rename from render/rect_test.go rename to lib/render/rect_test.go index 882b6fc..c6d8bb4 100644 --- a/render/rect_test.go +++ b/lib/render/rect_test.go @@ -4,7 +4,7 @@ import ( "strconv" "testing" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) func TestIntersection(t *testing.T) { diff --git a/render/sdl/canvas.go b/lib/render/sdl/canvas.go similarity index 97% rename from render/sdl/canvas.go rename to lib/render/sdl/canvas.go index 2272c49..0095585 100644 --- a/render/sdl/canvas.go +++ b/lib/render/sdl/canvas.go @@ -2,7 +2,7 @@ package sdl import ( - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" "github.com/veandco/go-sdl2/sdl" ) diff --git a/render/sdl/events.go b/lib/render/sdl/events.go similarity index 98% rename from render/sdl/events.go rename to lib/render/sdl/events.go index 5a8bad3..277bd4a 100644 --- a/render/sdl/events.go +++ b/lib/render/sdl/events.go @@ -3,7 +3,7 @@ package sdl import ( "errors" - "git.kirsle.net/apps/doodle/events" + "git.kirsle.net/apps/doodle/src/events" // XXX: break it up "github.com/veandco/go-sdl2/sdl" ) diff --git a/render/sdl/log.go b/lib/render/sdl/log.go similarity index 100% rename from render/sdl/log.go rename to lib/render/sdl/log.go diff --git a/render/sdl/sdl.go b/lib/render/sdl/sdl.go similarity index 96% rename from render/sdl/sdl.go rename to lib/render/sdl/sdl.go index 2b6bb8e..5abd80f 100644 --- a/render/sdl/sdl.go +++ b/lib/render/sdl/sdl.go @@ -4,8 +4,8 @@ package sdl import ( "time" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/events" "github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/ttf" ) diff --git a/render/sdl/text.go b/lib/render/sdl/text.go similarity index 97% rename from render/sdl/text.go rename to lib/render/sdl/text.go index dc00129..94b53bd 100644 --- a/render/sdl/text.go +++ b/lib/render/sdl/text.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/events" "github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/ttf" ) diff --git a/render/sdl/texture.go b/lib/render/sdl/texture.go similarity index 96% rename from render/sdl/texture.go rename to lib/render/sdl/texture.go index 192da48..b7843cc 100644 --- a/render/sdl/texture.go +++ b/lib/render/sdl/texture.go @@ -3,7 +3,7 @@ package sdl import ( "fmt" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" "github.com/veandco/go-sdl2/sdl" ) diff --git a/render/sdl/utils.go b/lib/render/sdl/utils.go similarity index 90% rename from render/sdl/utils.go rename to lib/render/sdl/utils.go index efcbf9c..3c7f260 100644 --- a/render/sdl/utils.go +++ b/lib/render/sdl/utils.go @@ -1,7 +1,7 @@ package sdl import ( - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" "github.com/veandco/go-sdl2/sdl" ) diff --git a/lib/ui/README.md b/lib/ui/README.md new file mode 100644 index 0000000..2db9b9f --- /dev/null +++ b/lib/ui/README.md @@ -0,0 +1,8 @@ +# Doodle UI + +This is a UI toolkit designed for Doodle but made generally available. + +## Ties to Break + +lib/ui and lib/render both currently import doodle/src/events as their only +dependency on Doodle. This module should be moved to a better common location. diff --git a/ui/button.go b/lib/ui/button.go similarity index 96% rename from ui/button.go rename to lib/ui/button.go index e7240ca..708c88a 100644 --- a/ui/button.go +++ b/lib/ui/button.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui/theme" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui/theme" ) // Button is a clickable button. diff --git a/ui/check_button.go b/lib/ui/check_button.go similarity index 96% rename from ui/check_button.go rename to lib/ui/check_button.go index dd56f58..838667c 100644 --- a/ui/check_button.go +++ b/lib/ui/check_button.go @@ -4,8 +4,8 @@ import ( "fmt" "strconv" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui/theme" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui/theme" ) // CheckButton implements a checkbox and radiobox widget. It's based on a diff --git a/ui/checkbox.go b/lib/ui/checkbox.go similarity index 97% rename from ui/checkbox.go rename to lib/ui/checkbox.go index 59b26fd..e98d7d2 100644 --- a/ui/checkbox.go +++ b/lib/ui/checkbox.go @@ -1,6 +1,6 @@ package ui -import "git.kirsle.net/apps/doodle/render" +import "git.kirsle.net/apps/doodle/lib/render" // Checkbox combines a CheckButton with a widget like a Label. type Checkbox struct { diff --git a/ui/debug.go b/lib/ui/debug.go similarity index 100% rename from ui/debug.go rename to lib/ui/debug.go diff --git a/ui/dragdrop.go b/lib/ui/dragdrop.go similarity index 100% rename from ui/dragdrop.go rename to lib/ui/dragdrop.go diff --git a/ui/frame.go b/lib/ui/frame.go similarity index 97% rename from ui/frame.go rename to lib/ui/frame.go index ee15af6..fe3be40 100644 --- a/ui/frame.go +++ b/lib/ui/frame.go @@ -3,7 +3,7 @@ package ui import ( "fmt" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Frame is a widget that contains other widgets. diff --git a/ui/frame_pack.go b/lib/ui/frame_pack.go similarity index 99% rename from ui/frame_pack.go rename to lib/ui/frame_pack.go index 228f8fc..7915567 100644 --- a/ui/frame_pack.go +++ b/lib/ui/frame_pack.go @@ -1,6 +1,8 @@ package ui -import "git.kirsle.net/apps/doodle/render" +import ( + "git.kirsle.net/apps/doodle/lib/render" +) // Pack provides configuration fields for Frame.Pack(). type Pack struct { diff --git a/ui/functions.go b/lib/ui/functions.go similarity index 93% rename from ui/functions.go rename to lib/ui/functions.go index bceca8b..9780a30 100644 --- a/ui/functions.go +++ b/lib/ui/functions.go @@ -1,6 +1,6 @@ package ui -import "git.kirsle.net/apps/doodle/render" +import "git.kirsle.net/apps/doodle/lib/render" // AbsolutePosition computes a widget's absolute X,Y position on the // window on screen by crawling its parent widget tree. diff --git a/ui/image.go b/lib/ui/image.go similarity index 97% rename from ui/image.go rename to lib/ui/image.go index 94e9993..32d57fd 100644 --- a/ui/image.go +++ b/lib/ui/image.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // ImageType for supported image formats. diff --git a/ui/label.go b/lib/ui/label.go similarity index 98% rename from ui/label.go rename to lib/ui/label.go index 8f3cfdf..0aefbc1 100644 --- a/ui/label.go +++ b/lib/ui/label.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // DefaultFont is the default font settings used for a Label. diff --git a/ui/log.go b/lib/ui/log.go similarity index 100% rename from ui/log.go rename to lib/ui/log.go diff --git a/ui/supervisor.go b/lib/ui/supervisor.go similarity index 98% rename from ui/supervisor.go rename to lib/ui/supervisor.go index 43ae0bd..4c599eb 100644 --- a/ui/supervisor.go +++ b/lib/ui/supervisor.go @@ -4,8 +4,8 @@ import ( "errors" "sync" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/events" ) // Event is a named event that the supervisor will send. diff --git a/ui/theme/theme.go b/lib/ui/theme/theme.go similarity index 72% rename from ui/theme/theme.go rename to lib/ui/theme/theme.go index 23c3186..d8e5d89 100644 --- a/ui/theme/theme.go +++ b/lib/ui/theme/theme.go @@ -1,6 +1,6 @@ package theme -import "git.kirsle.net/apps/doodle/render" +import "git.kirsle.net/apps/doodle/lib/render" // Color schemes. var ( @@ -8,5 +8,5 @@ var ( ButtonHoverColor = render.RGBA(200, 255, 255, 255) ButtonOutlineColor = render.Black - BorderColorOffset int32 = 40 + BorderColorOffset = 40 ) diff --git a/ui/widget.go b/lib/ui/widget.go similarity index 99% rename from ui/widget.go rename to lib/ui/widget.go index bcc6db8..e7b2328 100644 --- a/ui/widget.go +++ b/lib/ui/widget.go @@ -1,8 +1,8 @@ package ui import ( - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui/theme" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui/theme" ) // BorderStyle options for widget.SetBorderStyle() diff --git a/ui/window.go b/lib/ui/window.go similarity index 98% rename from ui/window.go rename to lib/ui/window.go index 4347c63..a956e7f 100644 --- a/ui/window.go +++ b/lib/ui/window.go @@ -3,7 +3,7 @@ package ui import ( "fmt" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Window is a frame with a title bar. diff --git a/pkg/wallpaper/texture.go b/pkg/wallpaper/texture.go index 71a81ad..45a6b23 100644 --- a/pkg/wallpaper/texture.go +++ b/pkg/wallpaper/texture.go @@ -7,8 +7,8 @@ import ( "image" "os" + "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/userdir" - "git.kirsle.net/apps/doodle/render" "golang.org/x/image/bmp" ) diff --git a/pkg/wallpaper/wallpaper.go b/pkg/wallpaper/wallpaper.go index 05885d0..98482a8 100644 --- a/pkg/wallpaper/wallpaper.go +++ b/pkg/wallpaper/wallpaper.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Wallpaper is a repeatable background image to go behind levels. diff --git a/pkg/wallpaper/wallpaper_test.go b/pkg/wallpaper/wallpaper_test.go index 72d3c1e..4efcecf 100644 --- a/pkg/wallpaper/wallpaper_test.go +++ b/pkg/wallpaper/wallpaper_test.go @@ -28,7 +28,7 @@ func TestWallpaper(t *testing.T) { img := image.NewRGBA(image.Rect(0, 0, width, height)) draw.Draw( // Corner: red - img, // dst Image + img, // dst Image image.Rect(0, 0, qWidth, qHeight), // r Rectangle image.NewUniform(red), // src Image image.Point{0, 0}, // sp Point diff --git a/render/sdl/fps.go b/render/sdl/fps.go deleted file mode 100644 index 4c625c9..0000000 --- a/render/sdl/fps.go +++ /dev/null @@ -1,22 +0,0 @@ -package sdl - -import ( - "git.kirsle.net/apps/doodle/level" -) - -// Frames to cache for FPS calculation. -const ( - maxSamples = 100 - TargetFPS = 1000 / 60 -) - -var ( - fpsCurrentTicks uint32 // current time we get sdl.GetTicks() - fpsLastTime uint32 // last time we printed the fpsCurrentTicks - fpsCurrent int - fpsFrames int - fpsSkipped uint32 - fpsInterval uint32 = 1000 -) - -var pixelHistory []level.Pixel diff --git a/balance/README.md b/src/balance/README.md similarity index 100% rename from balance/README.md rename to src/balance/README.md diff --git a/balance/debug.go b/src/balance/debug.go similarity index 90% rename from balance/debug.go rename to src/balance/debug.go index 1d630ab..0274b38 100644 --- a/balance/debug.go +++ b/src/balance/debug.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Debug related variables that can toggle on or off certain features and @@ -16,11 +16,11 @@ var ( ***************/ // Debug overlay (FPS etc.) settings. - DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf" - DebugFontSize = 15 - DebugLabelColor = render.MustHexColor("#FF9900") - DebugValueColor = render.MustHexColor("#00CCFF") - DebugStrokeDarken int32 = 80 + DebugFontFilename = "./fonts/DejaVuSans.ttf" + DebugFontSize = 16 + DebugLabelColor = render.MustHexColor("#FF9900") + DebugValueColor = render.MustHexColor("#00CCFF") + DebugStrokeDarken = 80 // Background color to use when exporting a drawing Chunk as a bitmap image // on disk. Default is white. Setting this to translucent yellow is a great diff --git a/balance/numbers.go b/src/balance/numbers.go similarity index 57% rename from balance/numbers.go rename to src/balance/numbers.go index 2149fca..14ee9f6 100644 --- a/balance/numbers.go +++ b/src/balance/numbers.go @@ -9,6 +9,16 @@ var ( // Speed to scroll a canvas with arrow keys in Edit Mode. CanvasScrollSpeed int32 = 8 + // Window scrolling behavior in Play Mode. + ScrollboxHoz = 64 // horizontal pixels close to canvas border + ScrollboxVert = 128 + ScrollMaxVelocity = 24 + + // Player speeds + PlayerMaxVelocity = 12 + PlayerAcceleration = 2 // pixels / second / second + Gravity = 2 + // Default chunk size for canvases. ChunkSize = 128 diff --git a/balance/shell.go b/src/balance/shell.go similarity index 92% rename from balance/shell.go rename to src/balance/shell.go index 12ef517..dbc3711 100644 --- a/balance/shell.go +++ b/src/balance/shell.go @@ -1,7 +1,7 @@ package balance import ( - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Shell related variables. diff --git a/balance/theme.go b/src/balance/theme.go similarity index 91% rename from balance/theme.go rename to src/balance/theme.go index 192a2ad..7da0987 100644 --- a/balance/theme.go +++ b/src/balance/theme.go @@ -1,8 +1,8 @@ package balance import ( - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" ) // Theme and appearance variables. diff --git a/commands.go b/src/commands.go similarity index 93% rename from commands.go rename to src/commands.go index d3d314a..324370a 100644 --- a/commands.go +++ b/src/commands.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "git.kirsle.net/apps/doodle/enum" + "git.kirsle.net/apps/doodle/src/enum" ) // Command is a parsed shell command. @@ -22,6 +22,17 @@ func (c Command) Run(d *Doodle) error { return nil } + // Cheats :P + if c.Raw == "unleash the beast" { + if fpsDoNotCap { + d.Flash("Reset frame rate throttle to factory default %d FPS", TargetFPS) + } else { + d.Flash("Unleashing as many frames as we can render!") + } + fpsDoNotCap = !fpsDoNotCap + return nil + } + switch c.Command { case "echo": d.Flash(c.ArgsLiteral) diff --git a/config.go b/src/config.go similarity index 100% rename from config.go rename to src/config.go diff --git a/doodads/actor.go b/src/doodads/actor.go similarity index 92% rename from doodads/actor.go rename to src/doodads/actor.go index 89707d1..cb0994c 100644 --- a/doodads/actor.go +++ b/src/doodads/actor.go @@ -1,8 +1,8 @@ package doodads import ( - "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/level" ) // Actor is a reusable run-time drawing component used in Doodle. Actors are an @@ -13,6 +13,9 @@ type Actor interface { // Position and velocity, not saved to disk. Position() render.Point Velocity() render.Point + SetVelocity(render.Point) + Acceleration() int + SetAcceleration(int) Size() render.Rect Grounded() bool SetGrounded(bool) @@ -20,9 +23,6 @@ type Actor interface { // Movement commands. MoveBy(render.Point) // Add {X,Y} to current Position. MoveTo(render.Point) // Set current Position to {X,Y}. - - // Implement the Draw function. - Draw(render.Engine) } // GetBoundingRect computes the full pairs of points for the collision box diff --git a/doodads/collision.go b/src/doodads/collision.go similarity index 87% rename from doodads/collision.go rename to src/doodads/collision.go index 739a7ad..833c1d9 100644 --- a/doodads/collision.go +++ b/src/doodads/collision.go @@ -1,8 +1,8 @@ package doodads import ( - "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/level" ) // Collide describes how a collision occurred. @@ -96,8 +96,15 @@ const ( Right ) -// CollidesWithGrid checks if a Doodad collides with level geometry. -func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { +/* +CollidesWithGrid checks if a Doodad collides with level geometry. + +The `target` is the point the actor wants to move to this tick. + +The `scroll` is the scroll offset of the canvas that is viewing the grid, so +that collision geometry can be offset properly on screen. +*/ +func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point, scroll render.Point) (*Collide, bool) { var ( P = d.Position() S = d.Size() @@ -109,14 +116,14 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli capHeight int32 // Stop vertical movement thru a ceiling capLeft int32 // Stop movement thru a wall capRight int32 - hitLeft bool // Has hit an obstacle on the left - hitRight bool // or right - hitFloor bool - capFloor int32 + capFloor int32 // Stop movement thru the floor + hitLeft bool // Has hit an obstacle on the left + hitRight bool // or right + hitFloor bool // or floor ) // Test all of the bounding boxes for a collision with level geometry. - if ok := result.ScanBoundingBox(GetBoundingRect(d), grid); ok { + if ok := result.ScanBoundingBox(GetBoundingRect(d).SubtractPoint(scroll), grid); ok { // We've already collided! Try to wiggle free. if result.Bottom { if !d.Grounded() { @@ -190,7 +197,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli Y: point.Y, W: S.W, H: S.H, - }, grid); has { + }.SubtractPoint(scroll), grid); has { if result.Bottom { if !hitFloor { hitFloor = true diff --git a/doodads/doodad.go b/src/doodads/doodad.go similarity index 91% rename from doodads/doodad.go rename to src/doodads/doodad.go index ea2fa33..71eb690 100644 --- a/doodads/doodad.go +++ b/src/doodads/doodad.go @@ -1,9 +1,9 @@ package doodads import ( - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/level" ) // Doodad is a reusable component for Levels that have scripts and graphics. diff --git a/src/doodads/drawing.go b/src/doodads/drawing.go new file mode 100644 index 0000000..fd74a2f --- /dev/null +++ b/src/doodads/drawing.go @@ -0,0 +1,91 @@ +package doodads + +import ( + "git.kirsle.net/apps/doodle/lib/render" + uuid "github.com/satori/go.uuid" +) + +// Drawing is a Doodad Actor that is based on drawings made inside the game. +type Drawing struct { + Doodad *Doodad + + id string + point render.Point + velocity render.Point + accel int + size render.Rect + grounded bool +} + +// NewDrawing creates a Drawing actor based on a Doodad drawing. If you pass +// an empty ID string, it will make a random UUIDv4 ID. +func NewDrawing(id string, doodad *Doodad) Drawing { + if id == "" { + id = uuid.Must(uuid.NewV4()).String() + } + return Drawing{ + id: id, + Doodad: doodad, + size: doodad.Rect(), + } +} + +// ID to get the Drawing ID. +func (d *Drawing) ID() string { + return d.id +} + +// Position returns the Drawing's position. +func (d *Drawing) Position() render.Point { + return d.point +} + +// Velocity returns the Drawing's velocity. +func (d *Drawing) Velocity() render.Point { + return d.velocity +} + +// SetVelocity to set the speed. +func (d *Drawing) SetVelocity(v render.Point) { + d.velocity = v +} + +// Acceleration returns the Drawing's acceleration. +func (d *Drawing) Acceleration() int { + return d.accel +} + +// SetAcceleration to set the acceleration. +func (d *Drawing) SetAcceleration(v int) { + d.accel = v +} + +// Size returns the Drawing's size. +func (d *Drawing) Size() render.Rect { + return d.size +} + +// Grounded returns whether the Drawing is standing on solid ground. +func (d *Drawing) Grounded() bool { + return d.grounded +} + +// SetGrounded sets the grounded state. +func (d *Drawing) SetGrounded(v bool) { + d.grounded = v +} + +// MoveBy a relative value. +func (d *Drawing) MoveBy(by render.Point) { + d.point.Add(by) +} + +// MoveTo an absolute world value. +func (d *Drawing) MoveTo(to render.Point) { + d.point = to +} + +// Draw the drawing. +func (d *Drawing) Draw(e render.Engine) { + +} diff --git a/src/doodads/dummy/dummy.go b/src/doodads/dummy/dummy.go new file mode 100644 index 0000000..7160f1b --- /dev/null +++ b/src/doodads/dummy/dummy.go @@ -0,0 +1,16 @@ +// Package dummy implements a dummy doodads.Drawing. +package dummy + +import "git.kirsle.net/apps/doodle/src/doodads" + +// Drawing is a dummy doodads.Drawing that has no data. +type Drawing struct { + doodads.Drawing +} + +// NewDrawing creates a new dummy drawing. +func NewDrawing(id string, doodad *doodads.Doodad) *Drawing { + return &Drawing{ + Drawing: doodads.NewDrawing(id, doodad), + } +} diff --git a/src/doodads/dummy/player.go b/src/doodads/dummy/player.go new file mode 100644 index 0000000..c0a69db --- /dev/null +++ b/src/doodads/dummy/player.go @@ -0,0 +1,11 @@ +// Package dummy implements a dummy doodads.Drawing. +package dummy + +import "git.kirsle.net/apps/doodle/src/doodads" + +// NewPlayer creates a dummy player object. +func NewPlayer() *Drawing { + return &Drawing{ + Drawing: doodads.NewDrawing("PLAYER", doodads.New(32)), + } +} diff --git a/doodads/json.go b/src/doodads/json.go similarity index 100% rename from doodads/json.go rename to src/doodads/json.go diff --git a/doodle.go b/src/doodle.go similarity index 89% rename from doodle.go rename to src/doodle.go index 4f129cf..540708e 100644 --- a/doodle.go +++ b/src/doodle.go @@ -5,16 +5,17 @@ import ( "strings" "time" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/enum" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/enum" + "git.kirsle.net/apps/doodle/src/events" "github.com/kirsle/golog" ) const ( // Version number. - Version = "0.0.1-alpha" + Version = "0.0.7-alpha" // TargetFPS is the frame rate to cap the game to. TargetFPS = 1000 / 60 // 60 FPS @@ -147,13 +148,15 @@ func (d *Doodle) Run() error { } // Delay to maintain the target frames per second. - elapsed := time.Now().Sub(start) - tmp := elapsed / time.Millisecond var delay uint32 - if TargetFPS-int(tmp) > 0 { // make sure it won't roll under - delay = uint32(TargetFPS - int(tmp)) + if !fpsDoNotCap { + elapsed := time.Now().Sub(start) + tmp := elapsed / time.Millisecond + if TargetFPS-int(tmp) > 0 { // make sure it won't roll under + delay = uint32(TargetFPS - int(tmp)) + } + d.Engine.Delay(delay) } - d.Engine.Delay(delay) // Track how long this frame took to measure FPS over time. d.TrackFPS(delay) @@ -216,7 +219,7 @@ func (d *Doodle) EditDrawing(filename string) error { func (d *Doodle) PlayLevel(filename string) error { log.Info("Loading level from file: %s", filename) scene := &PlayScene{ - Filename: filename, + Filename: userdir.LevelPath(filename), } d.Goto(scene) return nil diff --git a/draw/line.go b/src/draw/line.go similarity index 94% rename from draw/line.go rename to src/draw/line.go index a174164..bc98a87 100644 --- a/draw/line.go +++ b/src/draw/line.go @@ -3,7 +3,7 @@ package draw import ( "math" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Line is a generator that returns the X,Y coordinates to draw a line. diff --git a/draw/line_test.go b/src/draw/line_test.go similarity index 96% rename from draw/line_test.go rename to src/draw/line_test.go index eb1cabb..db03fb9 100644 --- a/draw/line_test.go +++ b/src/draw/line_test.go @@ -5,7 +5,7 @@ import ( "testing" "git.kirsle.net/apps/doodle/draw" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) func TestLine(t *testing.T) { diff --git a/editor_scene.go b/src/editor_scene.go similarity index 88% rename from editor_scene.go rename to src/editor_scene.go index b909686..f9e3d1d 100644 --- a/editor_scene.go +++ b/src/editor_scene.go @@ -7,12 +7,12 @@ import ( "os" "strings" - "git.kirsle.net/apps/doodle/doodads" - "git.kirsle.net/apps/doodle/enum" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/userdir" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/enum" + "git.kirsle.net/apps/doodle/src/events" + "git.kirsle.net/apps/doodle/src/level" ) // EditorScene manages the "Edit Level" game mode. @@ -31,6 +31,11 @@ type EditorScene struct { Level *level.Level Doodad *doodads.Doodad + // Custom debug overlay labels. + debTool *string + debSwatch *string + debWorldIndex *string + // Last saved filename by the user. filename string } @@ -42,6 +47,16 @@ func (s *EditorScene) Name() string { // Setup the editor scene. func (s *EditorScene) Setup(d *Doodle) error { + // Debug overlay labels. + s.debTool = new(string) + s.debSwatch = new(string) + s.debWorldIndex = new(string) + customDebugLabels = []debugLabel{ + {"Pixel:", s.debWorldIndex}, + {"Tool:", s.debTool}, + {"Swatch:", s.debSwatch}, + } + // Initialize the user interface. It references the palette and such so it // must be initialized after those things. s.d = d @@ -60,6 +75,7 @@ func (s *EditorScene) Setup(d *Doodle) error { if s.Level != nil { log.Debug("EditorScene.Setup: received level from scene caller") s.UI.Canvas.LoadLevel(d.Engine, s.Level) + s.UI.Canvas.InstallActors(s.Level.Actors) } else if s.filename != "" && s.OpenFile { log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) if err := s.LoadLevel(s.filename); err != nil { @@ -106,6 +122,11 @@ func (s *EditorScene) Setup(d *Doodle) error { // Loop the editor scene. func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { + // Update debug overlay labels. + *s.debTool = s.UI.Canvas.Tool.String() + *s.debSwatch = s.UI.Canvas.Palette.ActiveSwatch.Name + *s.debWorldIndex = s.UI.Canvas.WorldIndexAt(s.UI.cursor).String() + // Has the window been resized? if resized := ev.Resized.Read(); resized { w, h := d.Engine.WindowSize() @@ -261,6 +282,5 @@ func (s *EditorScene) SaveDoodad(filename string) error { // Destroy the scene. func (s *EditorScene) Destroy() error { - debugWorldIndex = render.Origin return nil } diff --git a/editor_scene_debug.go b/src/editor_scene_debug.go similarity index 85% rename from editor_scene_debug.go rename to src/editor_scene_debug.go index f2ceea0..c0ce20d 100644 --- a/editor_scene_debug.go +++ b/src/editor_scene_debug.go @@ -1,6 +1,6 @@ package doodle -import "git.kirsle.net/apps/doodle/uix" +import "git.kirsle.net/apps/doodle/src/uix" // TODO: build flags to not include this in production builds. // This adds accessors for private variables from the dev console. diff --git a/editor_ui.go b/src/editor_ui.go similarity index 97% rename from editor_ui.go rename to src/editor_ui.go index 0e0d6b5..6d97df2 100644 --- a/editor_ui.go +++ b/src/editor_ui.go @@ -5,15 +5,15 @@ import ( "path/filepath" "strconv" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/doodads" - "git.kirsle.net/apps/doodle/enum" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/userdir" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" - "git.kirsle.net/apps/doodle/uix" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/enum" + "git.kirsle.net/apps/doodle/src/events" + "git.kirsle.net/apps/doodle/src/level" + "git.kirsle.net/apps/doodle/src/uix" ) // Width of the panel frame. @@ -163,11 +163,10 @@ func (u *EditorUI) Loop(ev *events.State) error { // Update status bar labels. { - debugWorldIndex = u.Canvas.WorldIndexAt(u.cursor) u.StatusMouseText = fmt.Sprintf("Rel:(%d,%d) Abs:(%s)", ev.CursorX.Now, ev.CursorY.Now, - debugWorldIndex, + *u.Scene.debWorldIndex, ) u.StatusPaletteText = fmt.Sprintf("%s Tool", u.Canvas.Tool, diff --git a/editor_ui_doodad.go b/src/editor_ui_doodad.go similarity index 94% rename from editor_ui_doodad.go rename to src/editor_ui_doodad.go index 433641c..2a53c8b 100644 --- a/editor_ui_doodad.go +++ b/src/editor_ui_doodad.go @@ -7,12 +7,12 @@ package doodle import ( "fmt" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/doodads" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/userdir" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" - "git.kirsle.net/apps/doodle/uix" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/uix" ) // DraggableActor is a Doodad being dragged from the Doodad palette. diff --git a/editor_ui_palette.go b/src/editor_ui_palette.go similarity index 90% rename from editor_ui_palette.go rename to src/editor_ui_palette.go index d48c6fc..e41755f 100644 --- a/editor_ui_palette.go +++ b/src/editor_ui_palette.go @@ -1,9 +1,9 @@ package doodle import ( - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" ) // setupPaletteFrame configures the Color Palette tab for Edit Mode. diff --git a/enum/enum.go b/src/enum/enum.go similarity index 100% rename from enum/enum.go rename to src/enum/enum.go diff --git a/events/debug.go b/src/events/debug.go similarity index 100% rename from events/debug.go rename to src/events/debug.go diff --git a/events/events.go b/src/events/events.go similarity index 100% rename from events/events.go rename to src/events/events.go diff --git a/events/log.go b/src/events/log.go similarity index 100% rename from events/log.go rename to src/events/log.go diff --git a/events/types.go b/src/events/types.go similarity index 100% rename from events/types.go rename to src/events/types.go diff --git a/fps.go b/src/fps.go similarity index 69% rename from fps.go rename to src/fps.go index f1b6147..2ccdfce 100644 --- a/fps.go +++ b/src/fps.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/doodads" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/doodads" ) // Frames to cache for FPS calculation. @@ -33,37 +33,72 @@ var ( fpsFrames int fpsSkipped uint32 fpsInterval uint32 = 1000 + fpsDoNotCap bool // remove the FPS delay cap in Main Loop - // XXX: some opt-in WorldIndex variables for the debug overlay. - // This is the world pixel that the mouse cursor is over, - // the Cursor + Scroll position of the canvas. - debugWorldIndex render.Point + // Custom labels for scenes to add to the debug overlay view. + customDebugLabels []debugLabel ) +type debugLabel struct { + key string + variable *string +} + // DrawDebugOverlay draws the debug FPS text on the SDL canvas. func (d *Doodle) DrawDebugOverlay() { - if !d.Debug || !DebugOverlay { + if !DebugOverlay { return } + var framesSkipped = fmt.Sprintf("(skip: %dms)", fpsSkipped) + if fpsDoNotCap { + framesSkipped = "uncapped" + } + var ( darken = balance.DebugStrokeDarken Yoffset int32 = 20 // leave room for the menu bar - Xoffset int32 = 5 + Xoffset int32 = 20 keys = []string{ - " FPS:", + "FPS:", "Scene:", - "Pixel:", "Mouse:", } values = []string{ - fmt.Sprintf("%d (skip: %dms)", fpsCurrent, fpsSkipped), + fmt.Sprintf("%d %s", fpsCurrent, framesSkipped), d.Scene.Name(), - debugWorldIndex.String(), fmt.Sprintf("%d,%d", d.event.CursorX.Now, d.event.CursorY.Now), } ) + // Insert custom keys. + for _, custom := range customDebugLabels { + keys = append(keys, custom.key) + if custom.variable == nil { + values = append(values, "") + } else if len(*custom.variable) == 0 { + values = append(values, `""`) + } else { + values = append(values, *custom.variable) + } + } + + // Find the longest key. + var longest int + for _, key := range keys { + if len(key) > longest { + longest = len(key) + } + } + + // Space pad the keys to align them. + for i, key := range keys { + if len(key) < longest { + key = strings.Repeat(" ", longest-len(key)) + key + keys[i] = key + } + } + key := ui.NewLabel(ui.Label{ Text: strings.Join(keys, "\n"), Font: render.Text{ @@ -97,7 +132,7 @@ func (d *Doodle) DrawDebugOverlay() { // DrawCollisionBox draws the collision box around a Doodad. func (d *Doodle) DrawCollisionBox(actor doodads.Actor) { - if !d.Debug || !DebugCollision { + if !DebugCollision { return } diff --git a/guitest_scene.go b/src/guitest_scene.go similarity index 97% rename from guitest_scene.go rename to src/guitest_scene.go index 1b349eb..e949a41 100644 --- a/guitest_scene.go +++ b/src/guitest_scene.go @@ -3,10 +3,10 @@ package doodle import ( "fmt" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/events" ) // GUITestScene implements the main menu of Doodle. diff --git a/level/actors.go b/src/level/actors.go similarity index 96% rename from level/actors.go rename to src/level/actors.go index 332c044..caeb08d 100644 --- a/level/actors.go +++ b/src/level/actors.go @@ -1,7 +1,7 @@ package level import ( - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" uuid "github.com/satori/go.uuid" ) diff --git a/level/base_test.go b/src/level/base_test.go similarity index 100% rename from level/base_test.go rename to src/level/base_test.go diff --git a/level/chunk.go b/src/level/chunk.go similarity index 98% rename from level/chunk.go rename to src/level/chunk.go index a0aa78d..c2c79b5 100644 --- a/level/chunk.go +++ b/src/level/chunk.go @@ -7,9 +7,9 @@ import ( "math" "os" - "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/userdir" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/src/balance" "github.com/satori/go.uuid" "golang.org/x/image/bmp" ) diff --git a/level/chunk_map.go b/src/level/chunk_map.go similarity index 98% rename from level/chunk_map.go rename to src/level/chunk_map.go index 45a1075..a5b5def 100644 --- a/level/chunk_map.go +++ b/src/level/chunk_map.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // MapAccessor implements a chunk accessor by using a map of points to their diff --git a/level/chunk_test.go b/src/level/chunk_test.go similarity index 99% rename from level/chunk_test.go rename to src/level/chunk_test.go index 7ebf3ff..cad45aa 100644 --- a/level/chunk_test.go +++ b/src/level/chunk_test.go @@ -5,7 +5,7 @@ import ( "testing" "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Test the high level Chunker. diff --git a/level/chunker.go b/src/level/chunker.go similarity index 99% rename from level/chunker.go rename to src/level/chunker.go index 127c5db..0bcb0de 100644 --- a/level/chunker.go +++ b/src/level/chunker.go @@ -5,7 +5,7 @@ import ( "fmt" "math" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Chunker is the data structure that manages the chunks of a level, and diff --git a/level/chunker_test.go b/src/level/chunker_test.go similarity index 99% rename from level/chunker_test.go rename to src/level/chunker_test.go index d417a3d..c48a31b 100644 --- a/level/chunker_test.go +++ b/src/level/chunker_test.go @@ -5,7 +5,7 @@ import ( "testing" "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) func TestWorldSize(t *testing.T) { diff --git a/level/filesystem.go b/src/level/filesystem.go similarity index 100% rename from level/filesystem.go rename to src/level/filesystem.go diff --git a/level/json.go b/src/level/json.go similarity index 100% rename from level/json.go rename to src/level/json.go diff --git a/level/log.go b/src/level/log.go similarity index 100% rename from level/log.go rename to src/level/log.go diff --git a/level/page_type.go b/src/level/page_type.go similarity index 80% rename from level/page_type.go rename to src/level/page_type.go index 668c0a6..870f82c 100644 --- a/level/page_type.go +++ b/src/level/page_type.go @@ -28,5 +28,10 @@ const ( // - The wallpaper hoz mirrors Left along the X=Width plane // - The wallpaper vert mirrors Top along the Y=Width plane // - The wallpaper 180 rotates the Corner for opposite corners - Bordered + Bordered // TODO: to be implemented + + // FullPage treats the wallpaper image as a literal full scan of a page. + // - The level boundaries are fixed to the wallpaper image's dimensions. + // - Used to e.g. scan in real letterhead paper and draw a map on it. + FullPage // TODO: to be implemented ) diff --git a/level/palette.go b/src/level/palette.go similarity index 97% rename from level/palette.go rename to src/level/palette.go index 3061760..7c34b61 100644 --- a/level/palette.go +++ b/src/level/palette.go @@ -1,7 +1,7 @@ package level import ( - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // DefaultPalette returns a sensible default palette. diff --git a/level/swatch.go b/src/level/swatch.go similarity index 96% rename from level/swatch.go rename to src/level/swatch.go index 2dc1c46..a6304e0 100644 --- a/level/swatch.go +++ b/src/level/swatch.go @@ -3,7 +3,7 @@ package level import ( "fmt" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" ) // Swatch holds details about a single value in the palette. diff --git a/level/types.go b/src/level/types.go similarity index 96% rename from level/types.go rename to src/level/types.go index b603734..f91bd03 100644 --- a/level/types.go +++ b/src/level/types.go @@ -4,8 +4,8 @@ import ( "encoding/json" "fmt" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/balance" ) // Useful variables. diff --git a/log.go b/src/log.go similarity index 100% rename from log.go rename to src/log.go diff --git a/main_scene.go b/src/main_scene.go similarity index 91% rename from main_scene.go rename to src/main_scene.go index b559382..de72fee 100644 --- a/main_scene.go +++ b/src/main_scene.go @@ -1,10 +1,10 @@ package doodle import ( - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/events" ) // MainScene implements the main menu of Doodle. diff --git a/play_scene.go b/src/play_scene.go similarity index 50% rename from play_scene.go rename to src/play_scene.go index 981e336..ac19857 100644 --- a/play_scene.go +++ b/src/play_scene.go @@ -2,13 +2,15 @@ package doodle import ( "fmt" + "math" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/doodads" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/uix" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/doodads/dummy" + "git.kirsle.net/apps/doodle/src/events" + "git.kirsle.net/apps/doodle/src/level" + "git.kirsle.net/apps/doodle/src/uix" ) // PlayScene manages the "Edit Level" game mode. @@ -21,8 +23,14 @@ type PlayScene struct { d *Doodle drawing *uix.Canvas + // Custom debug labels. + debPosition *string + debViewport *string + debScroll *string + debWorldIndex *string + // Player character - Player doodads.Actor + Player *uix.Actor } // Name of the scene. @@ -33,6 +41,20 @@ func (s *PlayScene) Name() string { // Setup the play scene. func (s *PlayScene) Setup(d *Doodle) error { s.d = d + + // Initialize debug bound variables. + s.debPosition = new(string) + s.debViewport = new(string) + s.debScroll = new(string) + s.debWorldIndex = new(string) + customDebugLabels = []debugLabel{ + {"Pixel:", s.debWorldIndex}, + {"Player:", s.debPosition}, + {"Viewport:", s.debViewport}, + {"Scroll:", s.debScroll}, + } + + // Initialize the drawing canvas. s.drawing = uix.NewCanvas(balance.ChunkSize, false) s.drawing.MoveTo(render.Origin) s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height))) @@ -41,20 +63,26 @@ func (s *PlayScene) Setup(d *Doodle) error { // Given a filename or map data to play? if s.Level != nil { log.Debug("PlayScene.Setup: received level from scene caller") - s.drawing.LoadLevel(d.Engine, s.Level) + s.drawing.LoadLevel(s.d.Engine, s.Level) + s.drawing.InstallActors(s.Level.Actors) } else if s.Filename != "" { log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) s.LoadLevel(s.Filename) } - s.Player = doodads.NewPlayer() - if s.Level == nil { log.Debug("PlayScene.Setup: no grid given, initializing empty grid") s.Level = level.New() s.drawing.LoadLevel(d.Engine, s.Level) + s.drawing.InstallActors(s.Level.Actors) } + player := dummy.NewPlayer() + s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad) + s.Player.MoveTo(render.NewPoint(128, 128)) + s.drawing.AddActor(s.Player) + s.drawing.FollowActor = s.Player.ID() + d.Flash("Entered Play Mode. Press 'E' to edit this map.") return nil @@ -62,6 +90,23 @@ func (s *PlayScene) Setup(d *Doodle) error { // Loop the editor scene. func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { + // Update debug overlay variables. + *s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)).String() + *s.debPosition = s.Player.Position().String() + *s.debViewport = s.drawing.Viewport().String() + *s.debScroll = s.drawing.Scroll.String() + + // Has the window been resized? + if resized := ev.Resized.Read(); resized { + w, h := d.Engine.WindowSize() + if w != d.width || h != d.height { + d.width = w + d.height = h + s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height))) + return nil + } + } + // Switching to Edit Mode? if ev.KeyName.Read() == "e" { log.Info("Edit Mode, Go!") @@ -72,8 +117,11 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { return nil } - // s.drawing.Loop(ev) s.movePlayer(ev) + if err := s.drawing.Loop(ev); err != nil { + log.Error("Drawing Loop: %s", err) + } + return nil } @@ -86,9 +134,16 @@ func (s *PlayScene) Draw(d *Doodle) error { s.drawing.Present(d.Engine, s.drawing.Point()) // Draw our hero. - s.Player.Draw(d.Engine) + { + d.Engine.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{ + X: s.Player.Position().X, + Y: s.Player.Position().Y, + W: s.Player.Size().W, + H: s.Player.Size().H, + }) + } - // Draw out bounding boxes. + // Draw our bounding boxes. d.DrawCollisionBox(s.Player) return nil @@ -97,39 +152,49 @@ func (s *PlayScene) Draw(d *Doodle) error { // movePlayer updates the player's X,Y coordinate based on key pressed. func (s *PlayScene) movePlayer(ev *events.State) { delta := s.Player.Position() - var playerSpeed int32 = 8 - var gravity int32 = 2 + var playerSpeed = int32(balance.PlayerMaxVelocity) + var gravity = int32(balance.Gravity) + + var velocity render.Point if ev.Down.Now { - delta.Y += playerSpeed + velocity.Y = playerSpeed } if ev.Left.Now { - delta.X -= playerSpeed + velocity.X = -playerSpeed } if ev.Right.Now { - delta.X += playerSpeed + velocity.X = playerSpeed } if ev.Up.Now { - delta.Y -= playerSpeed + velocity.Y = -playerSpeed } // Apply gravity. // var onFloor bool - info, ok := doodads.CollidesWithGrid(s.Player, s.Level.Chunker, delta) + info, ok := doodads.CollidesWithGrid(s.Player, s.Level.Chunker, delta, s.drawing.Scroll) if ok { // Collision happened with world. } - delta = info.MoveTo + _ = info.MoveTo // Apply gravity if not 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 + velocity.Y += gravity } - s.Player.MoveTo(delta) + // Cap the max speed. + if int(math.Abs(float64(velocity.X))) > balance.PlayerMaxVelocity { + velocity.X = int32(balance.PlayerMaxVelocity) + } + if int(math.Abs(float64(velocity.Y))) > balance.PlayerMaxVelocity { + velocity.Y = int32(balance.PlayerMaxVelocity) + } + + s.Player.SetVelocity(velocity) } // LoadLevel loads a level from disk. @@ -143,6 +208,7 @@ func (s *PlayScene) LoadLevel(filename string) error { s.Level = level s.drawing.LoadLevel(s.d.Engine, s.Level) + s.drawing.InstallActors(s.Level.Actors) return nil } diff --git a/scene.go b/src/scene.go similarity index 84% rename from scene.go rename to src/scene.go index 1c04c43..1cedea1 100644 --- a/scene.go +++ b/src/scene.go @@ -1,6 +1,8 @@ package doodle -import "git.kirsle.net/apps/doodle/events" +import ( + "git.kirsle.net/apps/doodle/src/events" +) // Scene is an abstraction for a game mode in Doodle. The app points to one // scene at a time and that scene has control over the main loop, and its own @@ -20,6 +22,9 @@ type Scene interface { // Goto a scene. First it unloads the current scene. func (d *Doodle) Goto(scene Scene) error { + // Reset any custom debug overlay entries. + customDebugLabels = []debugLabel{} + // Teardown existing scene. if d.Scene != nil { d.Scene.Destroy() diff --git a/shell.go b/src/shell.go similarity index 97% rename from shell.go rename to src/shell.go index 23c828e..609ab0f 100644 --- a/shell.go +++ b/src/shell.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/events" "github.com/robertkrimen/otto" ) diff --git a/src/uix/actor.go b/src/uix/actor.go new file mode 100644 index 0000000..2335bac --- /dev/null +++ b/src/uix/actor.go @@ -0,0 +1,48 @@ +package uix + +import ( + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/level" + uuid "github.com/satori/go.uuid" +) + +// Actor is an object that marries together the three things that make a +// Doodad instance "tick" while inside a Canvas: +// +// - uix.Actor is a doodads.Drawing so it fulfills doodads.Actor to be a +// dynamic object during gameplay. +// - It has a pointer to the level.Actor indicating its static level data +// as defined in the map: its spawn coordinate and configuration. +// - A uix.Canvas that can present the actor's graphics to the screen. +type Actor struct { + doodads.Drawing + Actor *level.Actor + Canvas *Canvas +} + +// NewActor sets up a uix.Actor. +// If the id is blank, a new UUIDv4 is generated. +func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor { + if id == "" { + id = uuid.Must(uuid.NewV4()).String() + } + + size := int32(doodad.Layers[0].Chunker.Size) + can := NewCanvas(int(size), false) + can.Name = id + can.actor = levelActor + + // TODO: if the Background is render.Invisible it gets defaulted to + // White somewhere and the Doodad masks the level drawing behind it. + can.SetBackground(render.RGBA(0, 0, 1, 0)) + + can.LoadDoodad(doodad) + can.Resize(render.NewRect(size, size)) + + return &Actor{ + Drawing: doodads.NewDrawing(id, doodad), + Actor: levelActor, + Canvas: can, + } +} diff --git a/src/uix/canvas.go b/src/uix/canvas.go new file mode 100644 index 0000000..1ecf68c --- /dev/null +++ b/src/uix/canvas.go @@ -0,0 +1,289 @@ +package uix + +import ( + "fmt" + "os" + "strings" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/pkg/wallpaper" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/events" + "git.kirsle.net/apps/doodle/src/level" +) + +// Canvas is a custom ui.Widget that manages a single drawing. +type Canvas struct { + ui.Frame + Palette *level.Palette + + // Editable and Scrollable go hand in hand and, if you initialize a + // NewCanvas() with editable=true, they are both enabled. + Editable bool // Clicking will edit pixels of this canvas. + Scrollable bool // Cursor keys will scroll the viewport of this canvas. + + // Selected draw tool/mode, default Pencil, for editable canvases. + Tool Tool + + // MaskColor will force every pixel to render as this color regardless of + // the palette index of that pixel. Otherwise pixels behave the same and + // the palette does work as normal. Set to render.Invisible (zero value) + // to remove the mask. + MaskColor render.Color + + // Actor ID to follow the camera on automatically, i.e. the main player. + FollowActor string + + // Debug tools + // NoLimitScroll suppresses the scroll limit for bounded levels. + NoLimitScroll bool + + // Underlying chunk data for the drawing. + chunks *level.Chunker + + // Actors to superimpose on top of the drawing. + actor *level.Actor // if this canvas IS an actor + actors []*Actor + + // Wallpaper settings. + wallpaper *Wallpaper + + // When the Canvas wants to delete Actors, but ultimately it is upstream + // that controls the actors. Upstream should delete them and then reinstall + // the actor list from scratch. + OnDeleteActors func([]*level.Actor) + OnDragStart func(filename string) + + // Tracking pixels while editing. TODO: get rid of pixelHistory? + pixelHistory []*level.Pixel + lastPixel *level.Pixel + + // We inherit the ui.Widget which manages the width and height. + Scroll render.Point // Scroll offset for which parts of canvas are visible. +} + +// NewCanvas initializes a Canvas widget. +// +// If editable is true, Scrollable is also set to true, which means the arrow +// keys will scroll the canvas viewport which is desirable in Edit Mode. +func NewCanvas(size int, editable bool) *Canvas { + w := &Canvas{ + Editable: editable, + Scrollable: editable, + Palette: level.NewPalette(), + chunks: level.NewChunker(size), + actors: make([]*Actor, 0), + wallpaper: &Wallpaper{}, + } + w.setup() + w.IDFunc(func() string { + var attrs []string + + if w.Editable { + attrs = append(attrs, "editable") + } else { + attrs = append(attrs, "read-only") + } + + if w.Scrollable { + attrs = append(attrs, "scrollable") + } + + return fmt.Sprintf("Canvas<%d; %s>", size, strings.Join(attrs, "; ")) + }) + return w +} + +// Load initializes the Canvas using an existing Palette and Grid. +func (w *Canvas) Load(p *level.Palette, g *level.Chunker) { + w.Palette = p + w.chunks = g + + if len(w.Palette.Swatches) > 0 { + w.SetSwatch(w.Palette.Swatches[0]) + } +} + +// LoadLevel initializes a Canvas from a Level object. +func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) { + w.Load(level.Palette, level.Chunker) + + // TODO: wallpaper paths + filename := "assets/wallpapers/" + level.Wallpaper + if _, err := os.Stat(filename); os.IsNotExist(err) { + log.Error("LoadLevel: %s", err) + filename = "assets/wallpapers/notebook.png" // XXX TODO + } + + wp, err := wallpaper.FromFile(e, filename) + if err != nil { + log.Error("wallpaper FromFile(%s): %s", filename, err) + } + + w.wallpaper.maxWidth = level.MaxWidth + w.wallpaper.maxHeight = level.MaxHeight + err = w.wallpaper.Load(e, level.PageType, wp) + if err != nil { + log.Error("wallpaper Load: %s", err) + } +} + +// LoadDoodad initializes a Canvas from a Doodad object. +func (w *Canvas) LoadDoodad(d *doodads.Doodad) { + // TODO more safe + w.Load(d.Palette, d.Layers[0].Chunker) +} + +// SetSwatch changes the currently selected swatch for editing. +func (w *Canvas) SetSwatch(s *level.Swatch) { + w.Palette.ActiveSwatch = s +} + +// setup common configs between both initializers of the canvas. +func (w *Canvas) setup() { + // XXX: Debug code. + if balance.DebugCanvasBorder != render.Invisible { + w.Configure(ui.Config{ + BorderColor: balance.DebugCanvasBorder, + BorderSize: 2, + BorderStyle: ui.BorderSolid, + }) + } +} + +// Loop is called on the scene's event loop to handle mouse interaction with +// the canvas, i.e. to edit it. +func (w *Canvas) Loop(ev *events.State) error { + // Process the arrow keys scrolling the level in Edit Mode. + // canvas_scrolling.go + w.loopEditorScroll(ev) + if err := w.loopFollowActor(ev); err != nil { + log.Error("Follow actor: %s", err) // not fatal but nice to know + } + w.loopConstrainScroll() + + // Move any actors. + for _, a := range w.actors { + if v := a.Velocity(); v != render.Origin { + // orig := a.Drawing.Position() + a.MoveBy(v) + + // Keep them contained inside the level. + if w.wallpaper.pageType > level.Unbounded { + var ( + orig = w.WorldIndexAt(a.Drawing.Position()) + moveBy render.Point + size = a.Canvas.Size() + ) + + // Bound it on the top left edges. + if orig.X < 0 { + moveBy.X = -orig.X + } + if orig.Y < 0 { + moveBy.Y = -orig.Y + } + + // 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.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 !moveBy.IsZero() { + a.MoveBy(moveBy) + } + } + } + } + + // If the canvas is editable, only care if it's over our space. + if w.Editable { + cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) + if cursor.Inside(ui.AbsoluteRect(w)) { + return w.loopEditable(ev) + } + } + return nil +} + +// Viewport returns a rect containing the viewable drawing coordinates in this +// canvas. The X,Y values are the scroll offset (top left) and the W,H values +// are the scroll offset plus the width/height of the Canvas widget. +// +// The Viewport rect are the Absolute World Coordinates of the drawing that are +// visible inside the Canvas. The X,Y is the top left World Coordinate and the +// W,H are the bottom right World Coordinate, making this rect an absolute +// slice of the world. For a normal rect with a relative width and height, +// use ViewportRelative(). +// +// The rect X,Y are the negative Scroll Value. +// The rect W,H are the Canvas widget size minus the Scroll Value. +func (w *Canvas) Viewport() render.Rect { + var S = w.Size() + return render.Rect{ + X: -w.Scroll.X, + Y: -w.Scroll.Y, + W: S.W - w.Scroll.X, + H: S.H - w.Scroll.Y, + } +} + +// ViewportRelative returns a relative viewport where the Width and Height +// values are zero-relative: so you can use it with point.Inside(viewport) +// to see if a World Index point should be visible on screen. +// +// The rect X,Y are the negative Scroll Value +// The rect W,H are the Canvas widget size. +func (w *Canvas) ViewportRelative() render.Rect { + var S = w.Size() + return render.Rect{ + X: -w.Scroll.X, + Y: -w.Scroll.Y, + W: S.W, + H: S.H, + } +} + +// WorldIndexAt returns the World Index that corresponds to a Screen Pixel +// on the screen. If the screen pixel is the mouse coordinate (relative to +// the application window) this will return the World Index of the pixel below +// the mouse cursor. +func (w *Canvas) WorldIndexAt(screenPixel render.Point) render.Point { + var P = ui.AbsolutePosition(w) + return render.Point{ + X: screenPixel.X - P.X - w.Scroll.X, + Y: screenPixel.Y - P.Y - w.Scroll.Y, + } +} + +// Chunker returns the underlying Chunker object. +func (w *Canvas) Chunker() *level.Chunker { + return w.chunks +} + +// ScrollTo sets the viewport scroll position. +func (w *Canvas) ScrollTo(to render.Point) { + w.Scroll.X = to.X + w.Scroll.Y = to.Y +} + +// ScrollBy adjusts the viewport scroll position. +func (w *Canvas) ScrollBy(by render.Point) { + w.Scroll.Add(by) +} + +// Compute the canvas. +func (w *Canvas) Compute(e render.Engine) { + +} diff --git a/src/uix/canvas_actors.go b/src/uix/canvas_actors.go new file mode 100644 index 0000000..2d56c4e --- /dev/null +++ b/src/uix/canvas_actors.go @@ -0,0 +1,115 @@ +package uix + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/apps/doodle/src/doodads" + "git.kirsle.net/apps/doodle/src/level" +) + +// InstallActors adds external Actors to the canvas to be superimposed on top +// of the drawing. +func (w *Canvas) InstallActors(actors level.ActorMap) error { + w.actors = make([]*Actor, 0) + for id, actor := range actors { + doodad, err := doodads.LoadJSON(userdir.DoodadPath(actor.Filename)) + if err != nil { + return fmt.Errorf("InstallActors: %s", err) + } + + w.actors = append(w.actors, NewActor(id, actor, doodad)) + } + return nil +} + +// AddActor injects additional actors into the canvas, such as a Player doodad. +func (w *Canvas) AddActor(actor *Actor) error { + w.actors = append(w.actors, actor) + return nil +} + +// drawActors is a subroutine of Present() that superimposes the actors on top +// of the level drawing. +func (w *Canvas) drawActors(e render.Engine, p render.Point) { + var ( + Viewport = w.ViewportRelative() + S = w.Size() + ) + + // See if each Actor is in range of the Viewport. + for i, a := range w.actors { + if a == nil { + log.Error("Canvas.drawActors: null actor at index %d (of %d actors)", i, len(w.actors)) + continue + } + var ( + actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE + can = a.Canvas // Canvas widget that draws the actor + actorPoint = actor.Point // XXX TODO: DO NOT CHANGE + actorSize = can.Size() + ) + + // Create a box of World Coordinates that this actor occupies. The + // Actor X,Y from level data is already a World Coordinate; + // accomodate for the size of the Actor. + actorBox := render.Rect{ + X: actorPoint.X, + Y: actorPoint.Y, + W: actorSize.W, + H: actorSize.H, + } + + // Is any part of the actor visible? + if !Viewport.Intersects(actorBox) { + continue // not visible on screen + } + + drawAt := render.Point{ + X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1), + Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1), + } + resizeTo := actorSize + + // XXX TODO: when an Actor hits the left or top edge and shrinks, + // scrolling to offset that shrink is currently hard to solve. + scrollTo := render.Origin + + // Handle cropping and scaling if this Actor's canvas can't be + // completely visible within the parent. + if drawAt.X+resizeTo.W > p.X+S.W { + // Hitting the right edge, shrunk the width now. + delta := (drawAt.X + resizeTo.W) - (p.X + S.W) + resizeTo.W -= delta + } else if drawAt.X < p.X { + // Hitting the left edge. Cap the X coord and shrink the width. + delta := p.X - drawAt.X // positive number + drawAt.X = p.X + // scrollTo.X -= delta // TODO + resizeTo.W -= delta + } + + if drawAt.Y+resizeTo.H > p.Y+S.H { + // Hitting the bottom edge, shrink the height. + delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H) + resizeTo.H -= delta + } else if drawAt.Y < p.Y { + // Hitting the top edge. Cap the Y coord and shrink the height. + delta := p.Y - drawAt.Y + drawAt.Y = p.Y + // scrollTo.Y -= delta // TODO + resizeTo.H -= delta + } + + if resizeTo != actorSize { + can.Resize(resizeTo) + can.ScrollTo(scrollTo) + } + can.Present(e, drawAt) + + // Clean up the canvas size and offset. + can.Resize(actorSize) // restore original size in case cropped + can.ScrollTo(render.Origin) + } +} diff --git a/uix/canvas_editable.go b/src/uix/canvas_editable.go similarity index 94% rename from uix/canvas_editable.go rename to src/uix/canvas_editable.go index 184c5e2..15561d7 100644 --- a/uix/canvas_editable.go +++ b/src/uix/canvas_editable.go @@ -1,10 +1,10 @@ package uix 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" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/events" + "git.kirsle.net/apps/doodle/src/level" ) // loopEditable handles the Loop() part for editable canvases. diff --git a/src/uix/canvas_present.go b/src/uix/canvas_present.go new file mode 100644 index 0000000..cb6bb65 --- /dev/null +++ b/src/uix/canvas_present.go @@ -0,0 +1,171 @@ +package uix + +import ( + "fmt" + "strings" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" +) + +// Present the canvas. +func (w *Canvas) Present(e render.Engine, p render.Point) { + var ( + S = w.Size() + Viewport = w.Viewport() + ) + // w.MoveTo(p) // TODO: when uncommented the canvas will creep down the Workspace frame in EditorMode + w.DrawBox(e, p) + e.DrawBox(w.Background(), render.Rect{ + X: p.X + w.BoxThickness(1), + Y: p.Y + w.BoxThickness(1), + W: S.W - w.BoxThickness(2), + H: S.H - w.BoxThickness(2), + }) + + // Draw the wallpaper. + if w.wallpaper.Valid() { + err := w.PresentWallpaper(e, p) + if err != nil { + log.Error(err.Error()) + } + } + + // Get the chunks in the viewport and cache their textures. + for coord := range w.chunks.IterViewportChunks(Viewport) { + if chunk, ok := w.chunks.GetChunk(coord); ok { + var tex render.Texturer + if w.MaskColor != render.Invisible { + tex = chunk.TextureMasked(e, w.MaskColor) + } else { + tex = chunk.Texture(e) + } + src := render.Rect{ + W: tex.Size().W, + H: tex.Size().H, + } + + // If the source bitmap is already bigger than the Canvas widget + // into which it will render, cap the source width and height. + // This is especially useful for Doodad buttons because the drawing + // is bigger than the button. + if src.W > S.W { + src.W = S.W + } + if src.H > S.H { + src.H = S.H + } + + dst := render.Rect{ + X: p.X + w.Scroll.X + w.BoxThickness(1) + (coord.X * int32(chunk.Size)), + Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + (coord.Y * int32(chunk.Size)), + + // src.W and src.H will be AT MOST the full width and height of + // a Canvas widget. Subtract the scroll offset to keep it bounded + // visually on its right and bottom sides. + W: src.W, + H: src.H, + } + + // TODO: all this shit is in TrimBox(), make it DRY + + // If the destination width will cause it to overflow the widget + // box, trim off the right edge of the destination rect. + // + // Keep in mind we're dealing with chunks here, and a chunk is + // a small part of the image. Example: + // - Canvas is 800x600 (S.W=800 S.H=600) + // - Chunk wants to render at 790,0 width 100,100 or whatever + // dst={790, 0, 100, 100} + // - Chunk box would exceed 800px width (X=790 + W=100 == 890) + // - Find the delta how much it exceeds as negative (800 - 890 == -90) + // - Lower the Source and Dest rects by that delta size so they + // stay proportional and don't scale or anything dumb. + if dst.X+src.W > p.X+S.W { + // NOTE: delta is a negative number, + // so it will subtract from the width. + delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X) + src.W += delta + dst.W += delta + } + if dst.Y+src.H > p.Y+S.H { + // NOTE: delta is a negative number + delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y) + src.H += delta + dst.H += delta + } + + // The same for the top left edge, so the drawings don't overlap + // menu bars or left side toolbars. + // - Canvas was placed 80px from the left of the screen. + // Canvas.MoveTo(80, 0) + // - A texture wants to draw at 60, 0 which would cause it to + // overlap 20 pixels into the left toolbar. It needs to be cropped. + // - The delta is: p.X=80 - dst.X=60 == 20 + // - Set destination X to p.X to constrain it there: 20 + // - Subtract the delta from destination W so we don't scale it. + // - Add 20 to X of the source: the left edge of source is not visible + if dst.X < p.X { + // NOTE: delta is a positive number, + // so it will add to the destination coordinates. + delta := p.X - dst.X + dst.X = p.X + w.BoxThickness(1) + dst.W -= delta + src.X += delta + } + if dst.Y < p.Y { + delta := p.Y - dst.Y + dst.Y = p.Y + w.BoxThickness(1) + dst.H -= delta + src.Y += delta + } + + // Trim the destination width so it doesn't overlap the Canvas border. + if dst.W >= S.W-w.BoxThickness(1) { + dst.W = S.W - w.BoxThickness(1) + } + + e.Copy(tex, src, dst) + } + } + + w.drawActors(e, p) + + // XXX: Debug, show label in canvas corner. + if balance.DebugCanvasLabel { + rows := []string{ + w.Name, + + // XXX: debug options, uncomment for more details + + // Size of the canvas + // fmt.Sprintf("S=%d,%d", S.W, S.H), + + // Viewport of the canvas + // fmt.Sprintf("V=%d,%d:%d,%d", + // Viewport.X, Viewport.Y, + // Viewport.W, Viewport.H, + // ), + } + if w.actor != nil { + rows = append(rows, + fmt.Sprintf("WP=%s", w.actor.Point), + ) + } + label := ui.NewLabel(ui.Label{ + Text: strings.Join(rows, "\n"), + Font: render.Text{ + FontFilename: balance.ShellFontFilename, + Size: balance.ShellFontSizeSmall, + Color: render.White, + }, + }) + label.SetBackground(render.RGBA(0, 0, 50, 150)) + label.Compute(e) + label.Present(e, render.Point{ + X: p.X + S.W - label.Size().W - w.BoxThickness(1), + Y: p.Y + w.BoxThickness(1), + }) + } +} diff --git a/src/uix/canvas_scrolling.go b/src/uix/canvas_scrolling.go new file mode 100644 index 0000000..a51aca3 --- /dev/null +++ b/src/uix/canvas_scrolling.go @@ -0,0 +1,187 @@ +package uix + +import ( + "errors" + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/src/balance" + "git.kirsle.net/apps/doodle/src/events" + "git.kirsle.net/apps/doodle/src/level" +) + +/* +Loop() subroutine to scroll the canvas using arrow keys (for edit mode). + +If w.Scrollable is false this function won't do anything. + +Cursor keys will scroll the drawing by balance.CanvasScrollSpeed per tick. +If the level pageType is constrained, the scrollable viewport will be +constrained to fit the bounds of the level. + +The debug boolean `NoLimitScroll=true` will override the bounded level scroll +restriction and allow scrolling into out-of-bounds areas of the level. +*/ +func (w *Canvas) loopEditorScroll(ev *events.State) error { + if !w.Scrollable { + return errors.New("canvas not scrollable") + } + + // Arrow keys to scroll the view. + scrollBy := render.Point{} + if ev.Right.Now { + scrollBy.X -= balance.CanvasScrollSpeed + } else if ev.Left.Now { + scrollBy.X += balance.CanvasScrollSpeed + } + if ev.Down.Now { + scrollBy.Y -= balance.CanvasScrollSpeed + } else if ev.Up.Now { + scrollBy.Y += balance.CanvasScrollSpeed + } + if !scrollBy.IsZero() { + w.ScrollBy(scrollBy) + } + + return nil +} + +/* +Loop() subroutine to constrain the scrolled view to within a bounded level. +*/ +func (w *Canvas) loopConstrainScroll() error { + if w.NoLimitScroll { + return errors.New("NoLimitScroll enabled") + } + + var capped bool + + // Constrain the top and left edges. + if w.wallpaper.pageType > level.Unbounded { + if w.Scroll.X > 0 { + w.Scroll.X = 0 + capped = true + } + if w.Scroll.Y > 0 { + w.Scroll.Y = 0 + capped = true + } + } + + // Constrain the bottom and right for limited world sizes. + if w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 { + var ( + // TODO: downcast from int64! + mw = int32(w.wallpaper.maxWidth) + mh = int32(w.wallpaper.maxHeight) + Viewport = w.Viewport() + ) + if Viewport.W > mw { + delta := Viewport.W - mw + w.Scroll.X += delta + capped = true + } + if Viewport.H > mh { + delta := Viewport.H - mh + w.Scroll.Y += delta + capped = true + } + } + + if capped { + return errors.New("scroll limited by level constraint") + } + + return nil +} + +/* +Loop() subroutine for Play Mode to follow an actor in the camera's view. + +Does nothing if w.FollowActor is an empty string. Set it to the ID of an Actor +to follow. If the actor exists, the Canvas will scroll to keep it on the +screen. +*/ +func (w *Canvas) loopFollowActor(ev *events.State) error { + // Are we following an actor? + if w.FollowActor == "" { + return nil + } + + var ( + P = w.Point() + S = w.Size() + ) + + // Find the actor. + for _, actor := range w.actors { + if actor.ID() != w.FollowActor { + continue + } + + actor.Canvas.SetBorderSize(2) + actor.Canvas.SetBorderColor(render.Yellow) + actor.Canvas.SetBorderStyle(ui.BorderSolid) + + var ( + APosition = actor.Position() // relative to screen + APoint = actor.Drawing.Position() + ASize = actor.Drawing.Size() + scrollBy render.Point + ) + + // Scroll left + if APosition.X-P.X <= int32(balance.ScrollboxHoz) { + var delta = APoint.X - P.X + if delta > int32(balance.ScrollMaxVelocity) { + delta = int32(balance.ScrollMaxVelocity) + } + + if delta < 0 { + // constrain in case they're FAR OFF SCREEN so we don't flip back around + delta = -delta + } + scrollBy.X = delta + } + + // Scroll right + if APosition.X >= S.W-ASize.W-int32(balance.ScrollboxHoz) { + var delta = S.W - ASize.W - int32(balance.ScrollboxHoz) + if delta > int32(balance.ScrollMaxVelocity) { + delta = int32(balance.ScrollMaxVelocity) + } + scrollBy.X = -delta + } + + // Scroll up + if APosition.Y-P.Y <= int32(balance.ScrollboxVert) { + var delta = APoint.Y - P.Y + if delta > int32(balance.ScrollMaxVelocity) { + delta = int32(balance.ScrollMaxVelocity) + } + + if delta < 0 { + delta = -delta + } + scrollBy.Y = delta + } + + // Scroll down + if APosition.Y >= S.H-ASize.H-int32(balance.ScrollboxVert) { + var delta = S.H - ASize.H - int32(balance.ScrollboxVert) + if delta > int32(balance.ScrollMaxVelocity) { + delta = int32(balance.ScrollMaxVelocity) + } + scrollBy.Y = -delta + } + + if scrollBy != render.Origin { + w.ScrollBy(scrollBy) + } + + return nil + } + + return fmt.Errorf("actor ID '%s' not found in level", w.FollowActor) +} diff --git a/uix/canvas_wallpaper.go b/src/uix/canvas_wallpaper.go similarity index 97% rename from uix/canvas_wallpaper.go rename to src/uix/canvas_wallpaper.go index 827dadc..c1a9076 100644 --- a/uix/canvas_wallpaper.go +++ b/src/uix/canvas_wallpaper.go @@ -1,9 +1,9 @@ package uix import ( - "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/wallpaper" - "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/src/level" ) // Wallpaper configures the wallpaper in a Canvas. diff --git a/uix/draw_modes.go b/src/uix/draw_modes.go similarity index 100% rename from uix/draw_modes.go rename to src/uix/draw_modes.go diff --git a/uix/log.go b/src/uix/log.go similarity index 100% rename from uix/log.go rename to src/uix/log.go diff --git a/uix/canvas.go b/uix/canvas.go deleted file mode 100644 index b0c11ca..0000000 --- a/uix/canvas.go +++ /dev/null @@ -1,559 +0,0 @@ -package uix - -import ( - "fmt" - "os" - "strings" - - "git.kirsle.net/apps/doodle/balance" - "git.kirsle.net/apps/doodle/doodads" - "git.kirsle.net/apps/doodle/events" - "git.kirsle.net/apps/doodle/level" - "git.kirsle.net/apps/doodle/pkg/userdir" - "git.kirsle.net/apps/doodle/pkg/wallpaper" - "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" -) - -// Canvas is a custom ui.Widget that manages a single drawing. -type Canvas struct { - ui.Frame - Palette *level.Palette - - // Editable and Scrollable go hand in hand and, if you initialize a - // NewCanvas() with editable=true, they are both enabled. - Editable bool // Clicking will edit pixels of this canvas. - Scrollable bool // Cursor keys will scroll the viewport of this canvas. - - // Selected draw tool/mode, default Pencil, for editable canvases. - Tool Tool - - // MaskColor will force every pixel to render as this color regardless of - // the palette index of that pixel. Otherwise pixels behave the same and - // the palette does work as normal. Set to render.Invisible (zero value) - // to remove the mask. - MaskColor render.Color - - // Debug tools - // NoLimitScroll suppresses the scroll limit for bounded levels. - NoLimitScroll bool - - // Underlying chunk data for the drawing. - chunks *level.Chunker - - // Actors to superimpose on top of the drawing. - actor *level.Actor // if this canvas IS an actor - actors []*Actor - - // Wallpaper settings. - wallpaper *Wallpaper - - // When the Canvas wants to delete Actors, but ultimately it is upstream - // that controls the actors. Upstream should delete them and then reinstall - // the actor list from scratch. - OnDeleteActors func([]*level.Actor) - OnDragStart func(filename string) - - // Tracking pixels while editing. TODO: get rid of pixelHistory? - pixelHistory []*level.Pixel - lastPixel *level.Pixel - - // We inherit the ui.Widget which manages the width and height. - Scroll render.Point // Scroll offset for which parts of canvas are visible. -} - -// Actor is an instance of an actor with a Canvas attached. -type Actor struct { - Actor *level.Actor - Canvas *Canvas -} - -// NewCanvas initializes a Canvas widget. -// -// If editable is true, Scrollable is also set to true, which means the arrow -// keys will scroll the canvas viewport which is desirable in Edit Mode. -func NewCanvas(size int, editable bool) *Canvas { - w := &Canvas{ - Editable: editable, - Scrollable: editable, - Palette: level.NewPalette(), - chunks: level.NewChunker(size), - actors: make([]*Actor, 0), - wallpaper: &Wallpaper{}, - } - w.setup() - w.IDFunc(func() string { - var attrs []string - - if w.Editable { - attrs = append(attrs, "editable") - } else { - attrs = append(attrs, "read-only") - } - - if w.Scrollable { - attrs = append(attrs, "scrollable") - } - - return fmt.Sprintf("Canvas<%d; %s>", size, strings.Join(attrs, "; ")) - }) - return w -} - -// Load initializes the Canvas using an existing Palette and Grid. -func (w *Canvas) Load(p *level.Palette, g *level.Chunker) { - w.Palette = p - w.chunks = g - - if len(w.Palette.Swatches) > 0 { - w.SetSwatch(w.Palette.Swatches[0]) - } -} - -// LoadLevel initializes a Canvas from a Level object. -func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) { - w.Load(level.Palette, level.Chunker) - - // TODO: wallpaper paths - filename := "assets/wallpapers/" + level.Wallpaper - if _, err := os.Stat(filename); os.IsNotExist(err) { - log.Error("LoadLevel: %s", err) - filename = "assets/wallpapers/notebook.png" // XXX TODO - } - - wp, err := wallpaper.FromFile(e, filename) - if err != nil { - log.Error("wallpaper FromFile(%s): %s", filename, err) - } - - w.wallpaper.maxWidth = level.MaxWidth - w.wallpaper.maxHeight = level.MaxHeight - err = w.wallpaper.Load(e, level.PageType, wp) - if err != nil { - log.Error("wallpaper Load: %s", err) - } -} - -// LoadDoodad initializes a Canvas from a Doodad object. -func (w *Canvas) LoadDoodad(d *doodads.Doodad) { - // TODO more safe - w.Load(d.Palette, d.Layers[0].Chunker) -} - -// InstallActors adds external Actors to the canvas to be superimposed on top -// of the drawing. -func (w *Canvas) InstallActors(actors level.ActorMap) error { - w.actors = make([]*Actor, 0) - for id, actor := range actors { - log.Info("InstallActors: %s", id) - - doodad, err := doodads.LoadJSON(userdir.DoodadPath(actor.Filename)) - if err != nil { - return fmt.Errorf("InstallActors: %s", err) - } - - size := int32(doodad.Layers[0].Chunker.Size) - can := NewCanvas(int(size), false) - can.Name = id - can.actor = actor - // TODO: if the Background is render.Invisible it gets defaulted to - // White somewhere and the Doodad masks the level drawing behind it. - can.SetBackground(render.RGBA(0, 0, 1, 0)) - can.LoadDoodad(doodad) - can.Resize(render.NewRect(size, size)) - w.actors = append(w.actors, &Actor{ - Actor: actor, - Canvas: can, - }) - } - return nil -} - -// SetSwatch changes the currently selected swatch for editing. -func (w *Canvas) SetSwatch(s *level.Swatch) { - w.Palette.ActiveSwatch = s -} - -// setup common configs between both initializers of the canvas. -func (w *Canvas) setup() { - // XXX: Debug code. - if balance.DebugCanvasBorder != render.Invisible { - w.Configure(ui.Config{ - BorderColor: balance.DebugCanvasBorder, - BorderSize: 2, - BorderStyle: ui.BorderSolid, - }) - } -} - -// Loop is called on the scene's event loop to handle mouse interaction with -// the canvas, i.e. to edit it. -func (w *Canvas) Loop(ev *events.State) error { - if w.Scrollable { - // Arrow keys to scroll the view. - scrollBy := render.Point{} - if ev.Right.Now { - scrollBy.X -= balance.CanvasScrollSpeed - } else if ev.Left.Now { - scrollBy.X += balance.CanvasScrollSpeed - } - if ev.Down.Now { - scrollBy.Y -= balance.CanvasScrollSpeed - } else if ev.Up.Now { - scrollBy.Y += balance.CanvasScrollSpeed - } - if !scrollBy.IsZero() { - w.ScrollBy(scrollBy) - } - } - - // If the canvas is editable, only care if it's over our space. - if w.Editable { - cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) - if cursor.Inside(ui.AbsoluteRect(w)) { - return w.loopEditable(ev) - } - } - return nil -} - -// Viewport returns a rect containing the viewable drawing coordinates in this -// canvas. The X,Y values are the scroll offset (top left) and the W,H values -// are the scroll offset plus the width/height of the Canvas widget. -// -// The Viewport rect are the Absolute World Coordinates of the drawing that are -// visible inside the Canvas. The X,Y is the top left World Coordinate and the -// W,H are the bottom right World Coordinate, making this rect an absolute -// slice of the world. For a normal rect with a relative width and height, -// use ViewportRelative(). -// -// The rect X,Y are the negative Scroll Value. -// The rect W,H are the Canvas widget size minus the Scroll Value. -func (w *Canvas) Viewport() render.Rect { - var S = w.Size() - return render.Rect{ - X: -w.Scroll.X, - Y: -w.Scroll.Y, - W: S.W - w.Scroll.X, - H: S.H - w.Scroll.Y, - } -} - -// ViewportRelative returns a relative viewport where the Width and Height -// values are zero-relative: so you can use it with point.Inside(viewport) -// to see if a World Index point should be visible on screen. -// -// The rect X,Y are the negative Scroll Value -// The rect W,H are the Canvas widget size. -func (w *Canvas) ViewportRelative() render.Rect { - var S = w.Size() - return render.Rect{ - X: -w.Scroll.X, - Y: -w.Scroll.Y, - W: S.W, - H: S.H, - } -} - -// WorldIndexAt returns the World Index that corresponds to a Screen Pixel -// on the screen. If the screen pixel is the mouse coordinate (relative to -// the application window) this will return the World Index of the pixel below -// the mouse cursor. -func (w *Canvas) WorldIndexAt(screenPixel render.Point) render.Point { - var P = ui.AbsolutePosition(w) - return render.Point{ - X: screenPixel.X - P.X - w.Scroll.X, - Y: screenPixel.Y - P.Y - w.Scroll.Y, - } -} - -// Chunker returns the underlying Chunker object. -func (w *Canvas) Chunker() *level.Chunker { - return w.chunks -} - -// ScrollTo sets the viewport scroll position. -func (w *Canvas) ScrollTo(to render.Point) { - w.Scroll.X = to.X - w.Scroll.Y = to.Y -} - -// ScrollBy adjusts the viewport scroll position. -func (w *Canvas) ScrollBy(by render.Point) { - w.Scroll.Add(by) -} - -// Compute the canvas. -func (w *Canvas) Compute(e render.Engine) { - -} - -// Present the canvas. -func (w *Canvas) Present(e render.Engine, p render.Point) { - var ( - S = w.Size() - Viewport = w.Viewport() - ) - // w.MoveTo(p) // TODO: when uncommented the canvas will creep down the Workspace frame in EditorMode - w.DrawBox(e, p) - e.DrawBox(w.Background(), render.Rect{ - X: p.X + w.BoxThickness(1), - Y: p.Y + w.BoxThickness(1), - W: S.W - w.BoxThickness(2), - H: S.H - w.BoxThickness(2), - }) - - // Constrain the scroll view if the level is bounded. - if w.Scrollable && !w.NoLimitScroll { - // Constrain the top and left edges. - if w.wallpaper.pageType > level.Unbounded { - if w.Scroll.X > 0 { - w.Scroll.X = 0 - } - if w.Scroll.Y > 0 { - w.Scroll.Y = 0 - } - } - - // Constrain the bottom and right for limited world sizes. - if w.wallpaper.maxWidth > 0 && w.wallpaper.maxHeight > 0 { - var ( - // TODO: downcast from int64! - mw = int32(w.wallpaper.maxWidth) - mh = int32(w.wallpaper.maxHeight) - ) - if Viewport.W > mw { - delta := Viewport.W - mw - w.Scroll.X += delta - } - if Viewport.H > mh { - delta := Viewport.H - mh - w.Scroll.Y += delta - } - } - } - - // Draw the wallpaper. - if w.wallpaper.Valid() { - err := w.PresentWallpaper(e, p) - if err != nil { - log.Error(err.Error()) - } - } - - // Get the chunks in the viewport and cache their textures. - for coord := range w.chunks.IterViewportChunks(Viewport) { - if chunk, ok := w.chunks.GetChunk(coord); ok { - var tex render.Texturer - if w.MaskColor != render.Invisible { - tex = chunk.TextureMasked(e, w.MaskColor) - } else { - tex = chunk.Texture(e) - } - src := render.Rect{ - W: tex.Size().W, - H: tex.Size().H, - } - - // If the source bitmap is already bigger than the Canvas widget - // into which it will render, cap the source width and height. - // This is especially useful for Doodad buttons because the drawing - // is bigger than the button. - if src.W > S.W { - src.W = S.W - } - if src.H > S.H { - src.H = S.H - } - - dst := render.Rect{ - X: p.X + w.Scroll.X + w.BoxThickness(1) + (coord.X * int32(chunk.Size)), - Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + (coord.Y * int32(chunk.Size)), - - // src.W and src.H will be AT MOST the full width and height of - // a Canvas widget. Subtract the scroll offset to keep it bounded - // visually on its right and bottom sides. - W: src.W, - H: src.H, - } - - // TODO: all this shit is in TrimBox(), make it DRY - - // If the destination width will cause it to overflow the widget - // box, trim off the right edge of the destination rect. - // - // Keep in mind we're dealing with chunks here, and a chunk is - // a small part of the image. Example: - // - Canvas is 800x600 (S.W=800 S.H=600) - // - Chunk wants to render at 790,0 width 100,100 or whatever - // dst={790, 0, 100, 100} - // - Chunk box would exceed 800px width (X=790 + W=100 == 890) - // - Find the delta how much it exceeds as negative (800 - 890 == -90) - // - Lower the Source and Dest rects by that delta size so they - // stay proportional and don't scale or anything dumb. - if dst.X+src.W > p.X+S.W { - // NOTE: delta is a negative number, - // so it will subtract from the width. - delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X) - src.W += delta - dst.W += delta - } - if dst.Y+src.H > p.Y+S.H { - // NOTE: delta is a negative number - delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y) - src.H += delta - dst.H += delta - } - - // The same for the top left edge, so the drawings don't overlap - // menu bars or left side toolbars. - // - Canvas was placed 80px from the left of the screen. - // Canvas.MoveTo(80, 0) - // - A texture wants to draw at 60, 0 which would cause it to - // overlap 20 pixels into the left toolbar. It needs to be cropped. - // - The delta is: p.X=80 - dst.X=60 == 20 - // - Set destination X to p.X to constrain it there: 20 - // - Subtract the delta from destination W so we don't scale it. - // - Add 20 to X of the source: the left edge of source is not visible - if dst.X < p.X { - // NOTE: delta is a positive number, - // so it will add to the destination coordinates. - delta := p.X - dst.X - dst.X = p.X + w.BoxThickness(1) - dst.W -= delta - src.X += delta - } - if dst.Y < p.Y { - delta := p.Y - dst.Y - dst.Y = p.Y + w.BoxThickness(1) - dst.H -= delta - src.Y += delta - } - - // Trim the destination width so it doesn't overlap the Canvas border. - if dst.W >= S.W-w.BoxThickness(1) { - dst.W = S.W - w.BoxThickness(1) - } - - e.Copy(tex, src, dst) - } - } - - w.drawActors(e, p) - - // XXX: Debug, show label in canvas corner. - if balance.DebugCanvasLabel { - rows := []string{ - w.Name, - - // XXX: debug options, uncomment for more details - - // Size of the canvas - // fmt.Sprintf("S=%d,%d", S.W, S.H), - - // Viewport of the canvas - // fmt.Sprintf("V=%d,%d:%d,%d", - // Viewport.X, Viewport.Y, - // Viewport.W, Viewport.H, - // ), - } - if w.actor != nil { - rows = append(rows, - fmt.Sprintf("WP=%s", w.actor.Point), - ) - } - label := ui.NewLabel(ui.Label{ - Text: strings.Join(rows, "\n"), - Font: render.Text{ - FontFilename: balance.ShellFontFilename, - Size: balance.ShellFontSizeSmall, - Color: render.White, - }, - }) - label.SetBackground(render.RGBA(0, 0, 50, 150)) - label.Compute(e) - label.Present(e, render.Point{ - X: p.X + S.W - label.Size().W - w.BoxThickness(1), - Y: p.Y + w.BoxThickness(1), - }) - } -} - -// drawActors superimposes the actors on top of the drawing. -func (w *Canvas) drawActors(e render.Engine, p render.Point) { - var ( - Viewport = w.ViewportRelative() - S = w.Size() - ) - - // See if each Actor is in range of the Viewport. - for _, a := range w.actors { - var ( - actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE - can = a.Canvas // Canvas widget that draws the actor - actorPoint = actor.Point // XXX TODO: DO NOT CHANGE - actorSize = can.Size() - ) - - // Create a box of World Coordinates that this actor occupies. The - // Actor X,Y from level data is already a World Coordinate; - // accomodate for the size of the Actor. - actorBox := render.Rect{ - X: actorPoint.X, - Y: actorPoint.Y, - W: actorSize.W, - H: actorSize.H, - } - - // Is any part of the actor visible? - if !Viewport.Intersects(actorBox) { - continue // not visible on screen - } - - drawAt := render.Point{ - X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1), - Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1), - } - resizeTo := actorSize - - // XXX TODO: when an Actor hits the left or top edge and shrinks, - // scrolling to offset that shrink is currently hard to solve. - scrollTo := render.Origin - - // Handle cropping and scaling if this Actor's canvas can't be - // completely visible within the parent. - if drawAt.X+resizeTo.W > p.X+S.W { - // Hitting the right edge, shrunk the width now. - delta := (drawAt.X + resizeTo.W) - (p.X + S.W) - resizeTo.W -= delta - } else if drawAt.X < p.X { - // Hitting the left edge. Cap the X coord and shrink the width. - delta := p.X - drawAt.X // positive number - drawAt.X = p.X - // scrollTo.X -= delta // TODO - resizeTo.W -= delta - } - - if drawAt.Y+resizeTo.H > p.Y+S.H { - // Hitting the bottom edge, shrink the height. - delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H) - resizeTo.H -= delta - } else if drawAt.Y < p.Y { - // Hitting the top edge. Cap the Y coord and shrink the height. - delta := p.Y - drawAt.Y - drawAt.Y = p.Y - // scrollTo.Y -= delta // TODO - resizeTo.H -= delta - } - - if resizeTo != actorSize { - can.Resize(resizeTo) - can.ScrollTo(scrollTo) - } - can.Present(e, drawAt) - - // Clean up the canvas size and offset. - can.Resize(actorSize) // restore original size in case cropped - can.ScrollTo(render.Origin) - } -}