Port over code from old collision dev PR

stash-ui-rework
Noah 2019-04-09 19:17:56 -07:00
parent 8fc4f39da0
commit 5c08577214
21 changed files with 870 additions and 406 deletions

View File

@ -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

View File

@ -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

View File

@ -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{

View File

@ -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 = "<dynamic>"
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()
}

View File

@ -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
}

View File

@ -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
)

View File

@ -8,5 +8,5 @@ var (
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
ButtonOutlineColor = render.Black
BorderColorOffset int32 = 40
BorderColorOffset = 40
)

View File

@ -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

View File

@ -15,7 +15,7 @@ var (
ScrollMaxVelocity = 24
// Player speeds
PlayerMaxVelocity = 12
PlayerMaxVelocity = 8
PlayerAcceleration = 2
Gravity = 2

View File

@ -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

91
pkg/doodads/drawing.go Normal file
View File

@ -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) {
}

View File

@ -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),
}
}

View File

@ -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)),
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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()

48
pkg/uix/actor.go Normal file
View File

@ -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,
}
}

View File

@ -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)
}
}

116
pkg/uix/canvas_actors.go Normal file
View File

@ -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)
}
}

172
pkg/uix/canvas_present.go Normal file
View File

@ -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),
})
}
}

187
pkg/uix/canvas_scrolling.go Normal file
View File

@ -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)
}