diff --git a/Ideas.md b/Ideas.md index 3e4ca85..f100945 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,8 +244,9 @@ 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. + 4. **Bordered:** same as Bounded but with a different wallpaper behavior. + The bottom and right edges are covered with mirror images of the top and + left edges. * 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 @@ -264,6 +268,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 a1c0d6a..e3069a7 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,11 @@ SHELL := /bin/bash VERSION=$(shell grep -e 'Version =' pkg/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/cmd/doodad/main.go b/cmd/doodad/main.go index a8e96b1..5c60d7a 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/cmd/doodad/commands" doodle "git.kirsle.net/apps/doodle/pkg" "github.com/urfave/cli" ) -var Build = "N/A" +// Build variables. +var ( + Build = "N/A" + 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 7054714..d9fe3e2 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -1,59 +1,93 @@ package main import ( - "flag" + "fmt" + "log" + "os" "runtime" - - _ "image/png" + "sort" + "time" "git.kirsle.net/apps/doodle/lib/render/sdl" doodle "git.kirsle.net/apps/doodle/pkg" "git.kirsle.net/apps/doodle/pkg/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 { + 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/lib/render/color.go b/lib/render/color.go index c16cda2..7f1b2f5 100644 --- a/lib/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 value. +func (c Color) Transparentize(v int) Color { + return c.Add(0, 0, 0, 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/lib/render/sdl/fps.go b/lib/render/sdl/fps.go deleted file mode 100644 index cb503cb..0000000 --- a/lib/render/sdl/fps.go +++ /dev/null @@ -1,16 +0,0 @@ -package sdl - -// 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 -) diff --git a/lib/ui/theme/theme.go b/lib/ui/theme/theme.go index deaf460..d8e5d89 100644 --- a/lib/ui/theme/theme.go +++ b/lib/ui/theme/theme.go @@ -8,5 +8,5 @@ var ( ButtonHoverColor = render.RGBA(200, 255, 255, 255) ButtonOutlineColor = render.Black - BorderColorOffset int32 = 40 + BorderColorOffset = 40 ) diff --git a/pkg/balance/debug.go b/pkg/balance/debug.go index 50810f0..d9a20b2 100644 --- a/pkg/balance/debug.go +++ b/pkg/balance/debug.go @@ -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-Bold.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/pkg/balance/numbers.go b/pkg/balance/numbers.go index 0530578..7b1a7d8 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -15,7 +15,7 @@ var ( ScrollMaxVelocity = 24 // Player speeds - PlayerMaxVelocity = 12 + PlayerMaxVelocity = 8 PlayerAcceleration = 2 Gravity = 2 diff --git a/pkg/doodads/actor.go b/pkg/doodads/actor.go index 1f4f05c..233c79d 100644 --- a/pkg/doodads/actor.go +++ b/pkg/doodads/actor.go @@ -20,9 +20,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/pkg/doodads/drawing.go b/pkg/doodads/drawing.go new file mode 100644 index 0000000..fd74a2f --- /dev/null +++ b/pkg/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/pkg/doodads/dummy/dummy.go b/pkg/doodads/dummy/dummy.go new file mode 100644 index 0000000..ffcf784 --- /dev/null +++ b/pkg/doodads/dummy/dummy.go @@ -0,0 +1,16 @@ +// Package dummy implements a dummy doodads.Drawing. +package dummy + +import "git.kirsle.net/apps/doodle/pkg/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/pkg/doodads/dummy/player.go b/pkg/doodads/dummy/player.go new file mode 100644 index 0000000..7beac1b --- /dev/null +++ b/pkg/doodads/dummy/player.go @@ -0,0 +1,11 @@ +// Package dummy implements a dummy doodads.Drawing. +package dummy + +import "git.kirsle.net/apps/doodle/pkg/doodads" + +// NewPlayer creates a dummy player object. +func NewPlayer() *Drawing { + return &Drawing{ + Drawing: doodads.NewDrawing("PLAYER", doodads.New(32)), + } +} diff --git a/pkg/doodle.go b/pkg/doodle.go index e6316d9..2475a57 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -15,7 +15,7 @@ import ( 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 diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 23b2c4f..e72ca6f 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/doodads/dummy" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/uix" @@ -29,7 +30,7 @@ type PlayScene struct { debWorldIndex *string // Player character - Player doodads.Actor + Player *uix.Actor } // Name of the scene. @@ -63,19 +64,25 @@ func (s *PlayScene) Setup(d *Doodle) error { if s.Level != nil { log.Debug("PlayScene.Setup: received level from scene caller") s.drawing.LoadLevel(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 @@ -112,6 +119,10 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { // s.drawing.Loop(ev) s.movePlayer(ev) + if err := s.drawing.Loop(ev); err != nil { + log.Error("Drawing loop error: %s", err.Error()) + } + return nil } @@ -124,7 +135,12 @@ 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. d.DrawCollisionBox(s.Player) @@ -135,20 +151,26 @@ 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. @@ -165,8 +187,10 @@ func (s *PlayScene) movePlayer(ev *events.State) { // 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.SetVelocity(velocity) s.Player.MoveTo(delta) } @@ -181,6 +205,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/pkg/scene.go b/pkg/scene.go index b96d380..545aa8a 100644 --- a/pkg/scene.go +++ b/pkg/scene.go @@ -23,6 +23,9 @@ type Scene interface { // Goto a scene. First it unloads the current scene. func (d *Doodle) Goto(scene Scene) error { + // Clear any debug labels. + customDebugLabels = []debugLabel{} + // Teardown existing scene. if d.Scene != nil { d.Scene.Destroy() diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go new file mode 100644 index 0000000..51b5dfb --- /dev/null +++ b/pkg/uix/actor.go @@ -0,0 +1,48 @@ +package uix + +import ( + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/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/pkg/uix/canvas.go b/pkg/uix/canvas.go index 133b83a..c1dbc00 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -12,7 +12,6 @@ import ( "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" - "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/wallpaper" ) @@ -35,6 +34,9 @@ type Canvas struct { // 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 @@ -63,12 +65,6 @@ type Canvas struct { 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 @@ -141,35 +137,6 @@ func (w *Canvas) LoadDoodad(d *doodads.Doodad) { 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 @@ -190,21 +157,54 @@ func (w *Canvas) setup() { // 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) + // 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) + } + } } } @@ -288,273 +288,3 @@ func (w *Canvas) ScrollBy(by render.Point) { 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) - } -} diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go new file mode 100644 index 0000000..2f8eb21 --- /dev/null +++ b/pkg/uix/canvas_actors.go @@ -0,0 +1,116 @@ +package uix + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/userdir" +) + +// 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/pkg/uix/canvas_present.go b/pkg/uix/canvas_present.go new file mode 100644 index 0000000..711705d --- /dev/null +++ b/pkg/uix/canvas_present.go @@ -0,0 +1,172 @@ +package uix + +import ( + "fmt" + "strings" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/log" +) + +// 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/pkg/uix/canvas_scrolling.go b/pkg/uix/canvas_scrolling.go new file mode 100644 index 0000000..fcb62d3 --- /dev/null +++ b/pkg/uix/canvas_scrolling.go @@ -0,0 +1,187 @@ +package uix + +import ( + "errors" + "fmt" + + "git.kirsle.net/apps/doodle/lib/events" + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/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.Cyan) + 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) +}