Colliding Doodads #1
53
Ideas.md
53
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
|
* 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
|
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.
|
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
|
1. `Corner`: 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.
|
2. `Top`: Top right corner is the repeated "top of page" texture.
|
||||||
3. `BL`: Bottom left corner is the repeated "left of page" texture.
|
3. `Left`: Bottom left corner is the repeated "left of page" texture.
|
||||||
4. `BR`: Bottom right corner is the repeated background texture that extends
|
4. `Repeat`: Bottom right corner is the repeated background texture that extends
|
||||||
infinitely in all directions.
|
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
|
* 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
|
will be drawn and how the level boundaries may be constrained. There will be
|
||||||
four options:
|
four options:
|
||||||
|
@ -241,22 +244,14 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
|
||||||
wall.
|
wall.
|
||||||
3. **Bounded:** The map has a fixed width and height and is bounded on all
|
3. **Bounded:** The map has a fixed width and height and is bounded on all
|
||||||
four edges.
|
four edges.
|
||||||
4. **Bounded, Mirrored Wallpaper:** same as Bounded but with a different
|
4. **Bordered:** same as Bounded but the right and bottom edges are decorated
|
||||||
wallpaper behavior.
|
using mirrors of the top and left textures. This would look silly on notebook
|
||||||
* The page types will have their own behaviors with how wallpapers are drawn:
|
paper (having an extra red line and blank margin) but would work for
|
||||||
* **Unbounded:** only the `BR` texture from the wallpaper is used, repeated
|
wrap-around textures like paper placemats.
|
||||||
infinitely in the X and Y directions. The top-left, top, and left edge
|
5. **Full Page:** special level type that treats the wallpaper image as a
|
||||||
textures are not used.
|
literal full page scan to be drawn on top of. The level is bounded by the
|
||||||
* **No Negative Space:** the `TL` texture is drawn at coordinate `(0,0)`.
|
literal dimensions of the wallpaper image which is drawn once instead of
|
||||||
To its right, the `TR` texture is repeated forever in the X direction, and
|
cut up and tiled as per the usual behavior.
|
||||||
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.
|
|
||||||
* The game will come with a few built-in textures for levels to refer to by
|
* 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,
|
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
|
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
|
* The map author can also attach their own custom texture that will be included
|
||||||
inside the map file.
|
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
|
# Text Console
|
||||||
|
|
||||||
* Create a rudimentary dev console for entering text commands in-game. It
|
* Create a rudimentary dev console for entering text commands in-game. It
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -2,10 +2,11 @@ SHELL := /bin/bash
|
||||||
|
|
||||||
VERSION=$(shell grep -e 'Version' doodle.go | head -n 1 | cut -d '"' -f 2)
|
VERSION=$(shell grep -e 'Version' doodle.go | head -n 1 | cut -d '"' -f 2)
|
||||||
BUILD=$(shell git describe --always)
|
BUILD=$(shell git describe --always)
|
||||||
|
BUILD_DATE=$(shell date -Iseconds)
|
||||||
CURDIR=$(shell curdir)
|
CURDIR=$(shell curdir)
|
||||||
|
|
||||||
# Inject the build version (commit hash) into the executable.
|
# 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.
|
# `make setup` to set up a new environment, pull dependencies, etc.
|
||||||
.PHONY: setup
|
.PHONY: setup
|
||||||
|
|
BIN
assets/wallpapers/test-128x128.png
Normal file
BIN
assets/wallpapers/test-128x128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 375 B |
|
@ -11,10 +11,10 @@ import (
|
||||||
|
|
||||||
"image/png"
|
"image/png"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/src"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/src/doodads"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,22 +2,38 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle"
|
|
||||||
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
|
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
|
||||||
|
"git.kirsle.net/apps/doodle/src"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Build = "N/A"
|
// Build number is the git commit hash.
|
||||||
|
var (
|
||||||
|
Build = "<dynamic>"
|
||||||
|
BuildDate string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if BuildDate == "" {
|
||||||
|
BuildDate = time.Now().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "doodad"
|
app.Name = "doodad"
|
||||||
app.Usage = "command line interface for Doodle"
|
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{
|
app.Flags = []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
|
|
|
@ -1,40 +1,72 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle"
|
"git.kirsle.net/apps/doodle/lib/render/sdl"
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/src"
|
||||||
"git.kirsle.net/apps/doodle/render/sdl"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build number is the git commit hash.
|
// Build number is the git commit hash.
|
||||||
var Build string
|
|
||||||
|
|
||||||
// Command line args
|
|
||||||
var (
|
var (
|
||||||
debug bool
|
Build = "<dynamic>"
|
||||||
edit bool
|
BuildDate string
|
||||||
guitest bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&debug, "debug", false, "Debug mode")
|
if BuildDate == "" {
|
||||||
flag.BoolVar(&edit, "edit", false, "Edit the map given on the command line. Default is to play the map.")
|
BuildDate = time.Now().Format(time.RFC3339)
|
||||||
flag.BoolVar(&guitest, "guitest", false, "Enter the GUI Test scene.")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
args := flag.Args()
|
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.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
|
var filename string
|
||||||
if len(args) > 0 {
|
if c.NArg() > 0 {
|
||||||
filename = args[0]
|
filename = c.Args().Get(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SDL engine.
|
// SDL engine.
|
||||||
|
@ -44,16 +76,26 @@ func main() {
|
||||||
balance.Height,
|
balance.Height,
|
||||||
)
|
)
|
||||||
|
|
||||||
app := doodle.New(debug, engine)
|
game := doodle.New(c.Bool("debug"), engine)
|
||||||
app.SetupEngine()
|
game.SetupEngine()
|
||||||
if guitest {
|
if c.Bool("guitest") {
|
||||||
app.Goto(&doodle.GUITestScene{})
|
game.Goto(&doodle.GUITestScene{})
|
||||||
} else if filename != "" {
|
} else if filename != "" {
|
||||||
if edit {
|
if c.Bool("edit") {
|
||||||
app.EditFile(filename)
|
game.EditFile(filename)
|
||||||
} else {
|
} else {
|
||||||
app.PlayLevel(filename)
|
game.PlayLevel(filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.Run()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -156,15 +156,15 @@ func (c *Color) UnmarshalJSON(b []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a relative color value to the color.
|
// 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 (
|
var (
|
||||||
R = int32(c.Red) + r
|
R = int(c.Red) + r
|
||||||
G = int32(c.Green) + g
|
G = int(c.Green) + g
|
||||||
B = int32(c.Blue) + b
|
B = int(c.Blue) + b
|
||||||
A = int32(c.Alpha) + a
|
A = int(c.Alpha) + a
|
||||||
)
|
)
|
||||||
|
|
||||||
cap8 := func(v int32) uint8 {
|
cap8 := func(v int) uint8 {
|
||||||
if v > 255 {
|
if v > 255 {
|
||||||
v = 255
|
v = 255
|
||||||
} else if v < 0 {
|
} else if v < 0 {
|
||||||
|
@ -182,11 +182,22 @@ func (c Color) Add(r, g, b, a int32) Color {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lighten a color value.
|
// 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)
|
return c.Add(v, v, v, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darken a color value.
|
// 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)
|
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
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"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
|
// 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 {
|
func (r Rect) AddPoint(other Point) Rect {
|
||||||
return Rect{
|
return Rect{
|
||||||
X: r.X + other.X,
|
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.
|
// Text holds information for drawing text.
|
||||||
type Text struct {
|
type Text struct {
|
||||||
Text string
|
Text string
|
|
@ -69,12 +69,21 @@ func (p Point) Inside(r Rect) bool {
|
||||||
(p.Y >= y1 && p.Y <= y2))
|
(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) {
|
func (p *Point) Add(other Point) {
|
||||||
p.X += other.X
|
p.X += other.X
|
||||||
p.Y += other.Y
|
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
|
// MarshalText to convert the point into text so that a render.Point may be used
|
||||||
// as a map key and serialized to JSON.
|
// as a map key and serialized to JSON.
|
||||||
func (p *Point) MarshalText() ([]byte, error) {
|
func (p *Point) MarshalText() ([]byte, error) {
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPointInside(t *testing.T) {
|
func TestPointInside(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIntersection(t *testing.T) {
|
func TestIntersection(t *testing.T) {
|
|
@ -2,7 +2,7 @@
|
||||||
package sdl
|
package sdl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ package sdl
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/src/events" // XXX: break it up
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,8 +4,8 @@ package sdl
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
"github.com/veandco/go-sdl2/ttf"
|
"github.com/veandco/go-sdl2/ttf"
|
||||||
)
|
)
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
"github.com/veandco/go-sdl2/ttf"
|
"github.com/veandco/go-sdl2/ttf"
|
||||||
)
|
)
|
|
@ -3,7 +3,7 @@ package sdl
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package sdl
|
package sdl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
)
|
)
|
||||||
|
|
8
lib/ui/README.md
Normal file
8
lib/ui/README.md
Normal file
|
@ -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.
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/ui/theme"
|
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Button is a clickable button.
|
// Button is a clickable button.
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/ui/theme"
|
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
|
@ -1,6 +1,6 @@
|
||||||
package ui
|
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.
|
// Checkbox combines a CheckButton with a widget like a Label.
|
||||||
type Checkbox struct {
|
type Checkbox struct {
|
|
@ -3,7 +3,7 @@ package ui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frame is a widget that contains other widgets.
|
// Frame is a widget that contains other widgets.
|
|
@ -1,6 +1,8 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "git.kirsle.net/apps/doodle/render"
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
)
|
||||||
|
|
||||||
// Pack provides configuration fields for Frame.Pack().
|
// Pack provides configuration fields for Frame.Pack().
|
||||||
type Pack struct {
|
type Pack struct {
|
|
@ -1,6 +1,6 @@
|
||||||
package ui
|
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
|
// AbsolutePosition computes a widget's absolute X,Y position on the
|
||||||
// window on screen by crawling its parent widget tree.
|
// window on screen by crawling its parent widget tree.
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageType for supported image formats.
|
// ImageType for supported image formats.
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultFont is the default font settings used for a Label.
|
// DefaultFont is the default font settings used for a Label.
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event is a named event that the supervisor will send.
|
// Event is a named event that the supervisor will send.
|
|
@ -1,6 +1,6 @@
|
||||||
package theme
|
package theme
|
||||||
|
|
||||||
import "git.kirsle.net/apps/doodle/render"
|
import "git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
|
||||||
// Color schemes.
|
// Color schemes.
|
||||||
var (
|
var (
|
||||||
|
@ -8,5 +8,5 @@ var (
|
||||||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
||||||
ButtonOutlineColor = render.Black
|
ButtonOutlineColor = render.Black
|
||||||
|
|
||||||
BorderColorOffset int32 = 40
|
BorderColorOffset = 40
|
||||||
)
|
)
|
|
@ -1,8 +1,8 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/ui/theme"
|
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BorderStyle options for widget.SetBorderStyle()
|
// BorderStyle options for widget.SetBorderStyle()
|
|
@ -3,7 +3,7 @@ package ui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Window is a frame with a title bar.
|
// Window is a frame with a title bar.
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"image"
|
"image"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wallpaper is a repeatable background image to go behind levels.
|
// Wallpaper is a repeatable background image to go behind levels.
|
||||||
|
|
|
@ -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
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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
|
// Debug related variables that can toggle on or off certain features and
|
||||||
|
@ -16,11 +16,11 @@ var (
|
||||||
***************/
|
***************/
|
||||||
|
|
||||||
// Debug overlay (FPS etc.) settings.
|
// Debug overlay (FPS etc.) settings.
|
||||||
DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf"
|
DebugFontFilename = "./fonts/DejaVuSans.ttf"
|
||||||
DebugFontSize = 15
|
DebugFontSize = 16
|
||||||
DebugLabelColor = render.MustHexColor("#FF9900")
|
DebugLabelColor = render.MustHexColor("#FF9900")
|
||||||
DebugValueColor = render.MustHexColor("#00CCFF")
|
DebugValueColor = render.MustHexColor("#00CCFF")
|
||||||
DebugStrokeDarken int32 = 80
|
DebugStrokeDarken = 80
|
||||||
|
|
||||||
// Background color to use when exporting a drawing Chunk as a bitmap image
|
// 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
|
// on disk. Default is white. Setting this to translucent yellow is a great
|
|
@ -9,6 +9,16 @@ var (
|
||||||
// Speed to scroll a canvas with arrow keys in Edit Mode.
|
// Speed to scroll a canvas with arrow keys in Edit Mode.
|
||||||
CanvasScrollSpeed int32 = 8
|
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.
|
// Default chunk size for canvases.
|
||||||
ChunkSize = 128
|
ChunkSize = 128
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package balance
|
package balance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Shell related variables.
|
// Shell related variables.
|
|
@ -1,8 +1,8 @@
|
||||||
package balance
|
package balance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Theme and appearance variables.
|
// Theme and appearance variables.
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
"git.kirsle.net/apps/doodle/src/enum"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command is a parsed shell command.
|
// Command is a parsed shell command.
|
||||||
|
@ -22,6 +22,17 @@ func (c Command) Run(d *Doodle) error {
|
||||||
return nil
|
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 {
|
switch c.Command {
|
||||||
case "echo":
|
case "echo":
|
||||||
d.Flash(c.ArgsLiteral)
|
d.Flash(c.ArgsLiteral)
|
|
@ -1,8 +1,8 @@
|
||||||
package doodads
|
package doodads
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Actor is a reusable run-time drawing component used in Doodle. Actors are an
|
// 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 and velocity, not saved to disk.
|
||||||
Position() render.Point
|
Position() render.Point
|
||||||
Velocity() render.Point
|
Velocity() render.Point
|
||||||
|
SetVelocity(render.Point)
|
||||||
|
Acceleration() int
|
||||||
|
SetAcceleration(int)
|
||||||
Size() render.Rect
|
Size() render.Rect
|
||||||
Grounded() bool
|
Grounded() bool
|
||||||
SetGrounded(bool)
|
SetGrounded(bool)
|
||||||
|
@ -20,9 +23,6 @@ type Actor interface {
|
||||||
// Movement commands.
|
// Movement commands.
|
||||||
MoveBy(render.Point) // Add {X,Y} to current Position.
|
MoveBy(render.Point) // Add {X,Y} to current Position.
|
||||||
MoveTo(render.Point) // Set current Position to {X,Y}.
|
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
|
// GetBoundingRect computes the full pairs of points for the collision box
|
|
@ -1,8 +1,8 @@
|
||||||
package doodads
|
package doodads
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collide describes how a collision occurred.
|
// Collide describes how a collision occurred.
|
||||||
|
@ -96,8 +96,15 @@ const (
|
||||||
Right
|
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 (
|
var (
|
||||||
P = d.Position()
|
P = d.Position()
|
||||||
S = d.Size()
|
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
|
capHeight int32 // Stop vertical movement thru a ceiling
|
||||||
capLeft int32 // Stop movement thru a wall
|
capLeft int32 // Stop movement thru a wall
|
||||||
capRight int32
|
capRight int32
|
||||||
|
capFloor int32 // Stop movement thru the floor
|
||||||
hitLeft bool // Has hit an obstacle on the left
|
hitLeft bool // Has hit an obstacle on the left
|
||||||
hitRight bool // or right
|
hitRight bool // or right
|
||||||
hitFloor bool
|
hitFloor bool // or floor
|
||||||
capFloor int32
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test all of the bounding boxes for a collision with level geometry.
|
// 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.
|
// We've already collided! Try to wiggle free.
|
||||||
if result.Bottom {
|
if result.Bottom {
|
||||||
if !d.Grounded() {
|
if !d.Grounded() {
|
||||||
|
@ -190,7 +197,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
|
||||||
Y: point.Y,
|
Y: point.Y,
|
||||||
W: S.W,
|
W: S.W,
|
||||||
H: S.H,
|
H: S.H,
|
||||||
}, grid); has {
|
}.SubtractPoint(scroll), grid); has {
|
||||||
if result.Bottom {
|
if result.Bottom {
|
||||||
if !hitFloor {
|
if !hitFloor {
|
||||||
hitFloor = true
|
hitFloor = true
|
|
@ -1,9 +1,9 @@
|
||||||
package doodads
|
package doodads
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Doodad is a reusable component for Levels that have scripts and graphics.
|
// Doodad is a reusable component for Levels that have scripts and graphics.
|
91
src/doodads/drawing.go
Normal file
91
src/doodads/drawing.go
Normal 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) {
|
||||||
|
|
||||||
|
}
|
16
src/doodads/dummy/dummy.go
Normal file
16
src/doodads/dummy/dummy.go
Normal file
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
11
src/doodads/dummy/player.go
Normal file
11
src/doodads/dummy/player.go
Normal file
|
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,16 +5,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/enum"
|
||||||
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
"github.com/kirsle/golog"
|
"github.com/kirsle/golog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Version number.
|
// Version number.
|
||||||
Version = "0.0.1-alpha"
|
Version = "0.0.7-alpha"
|
||||||
|
|
||||||
// TargetFPS is the frame rate to cap the game to.
|
// TargetFPS is the frame rate to cap the game to.
|
||||||
TargetFPS = 1000 / 60 // 60 FPS
|
TargetFPS = 1000 / 60 // 60 FPS
|
||||||
|
@ -147,13 +148,15 @@ func (d *Doodle) Run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay to maintain the target frames per second.
|
// Delay to maintain the target frames per second.
|
||||||
|
var delay uint32
|
||||||
|
if !fpsDoNotCap {
|
||||||
elapsed := time.Now().Sub(start)
|
elapsed := time.Now().Sub(start)
|
||||||
tmp := elapsed / time.Millisecond
|
tmp := elapsed / time.Millisecond
|
||||||
var delay uint32
|
|
||||||
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under
|
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under
|
||||||
delay = uint32(TargetFPS - int(tmp))
|
delay = uint32(TargetFPS - int(tmp))
|
||||||
}
|
}
|
||||||
d.Engine.Delay(delay)
|
d.Engine.Delay(delay)
|
||||||
|
}
|
||||||
|
|
||||||
// Track how long this frame took to measure FPS over time.
|
// Track how long this frame took to measure FPS over time.
|
||||||
d.TrackFPS(delay)
|
d.TrackFPS(delay)
|
||||||
|
@ -216,7 +219,7 @@ func (d *Doodle) EditDrawing(filename string) error {
|
||||||
func (d *Doodle) PlayLevel(filename string) error {
|
func (d *Doodle) PlayLevel(filename string) error {
|
||||||
log.Info("Loading level from file: %s", filename)
|
log.Info("Loading level from file: %s", filename)
|
||||||
scene := &PlayScene{
|
scene := &PlayScene{
|
||||||
Filename: filename,
|
Filename: userdir.LevelPath(filename),
|
||||||
}
|
}
|
||||||
d.Goto(scene)
|
d.Goto(scene)
|
||||||
return nil
|
return nil
|
|
@ -3,7 +3,7 @@ package draw
|
||||||
import (
|
import (
|
||||||
"math"
|
"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.
|
// Line is a generator that returns the X,Y coordinates to draw a line.
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/draw"
|
"git.kirsle.net/apps/doodle/draw"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLine(t *testing.T) {
|
func TestLine(t *testing.T) {
|
|
@ -7,12 +7,12 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
|
||||||
"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/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.
|
// EditorScene manages the "Edit Level" game mode.
|
||||||
|
@ -31,6 +31,11 @@ type EditorScene struct {
|
||||||
Level *level.Level
|
Level *level.Level
|
||||||
Doodad *doodads.Doodad
|
Doodad *doodads.Doodad
|
||||||
|
|
||||||
|
// Custom debug overlay labels.
|
||||||
|
debTool *string
|
||||||
|
debSwatch *string
|
||||||
|
debWorldIndex *string
|
||||||
|
|
||||||
// Last saved filename by the user.
|
// Last saved filename by the user.
|
||||||
filename string
|
filename string
|
||||||
}
|
}
|
||||||
|
@ -42,6 +47,16 @@ func (s *EditorScene) Name() string {
|
||||||
|
|
||||||
// Setup the editor scene.
|
// Setup the editor scene.
|
||||||
func (s *EditorScene) Setup(d *Doodle) error {
|
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
|
// Initialize the user interface. It references the palette and such so it
|
||||||
// must be initialized after those things.
|
// must be initialized after those things.
|
||||||
s.d = d
|
s.d = d
|
||||||
|
@ -60,6 +75,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
if s.Level != nil {
|
if s.Level != nil {
|
||||||
log.Debug("EditorScene.Setup: received level from scene caller")
|
log.Debug("EditorScene.Setup: received level from scene caller")
|
||||||
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
|
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
|
||||||
|
s.UI.Canvas.InstallActors(s.Level.Actors)
|
||||||
} else if s.filename != "" && s.OpenFile {
|
} else if s.filename != "" && s.OpenFile {
|
||||||
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
|
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
|
||||||
if err := s.LoadLevel(s.filename); err != nil {
|
if err := s.LoadLevel(s.filename); err != nil {
|
||||||
|
@ -106,6 +122,11 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
|
|
||||||
// Loop the editor scene.
|
// Loop the editor scene.
|
||||||
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
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?
|
// Has the window been resized?
|
||||||
if resized := ev.Resized.Read(); resized {
|
if resized := ev.Resized.Read(); resized {
|
||||||
w, h := d.Engine.WindowSize()
|
w, h := d.Engine.WindowSize()
|
||||||
|
@ -261,6 +282,5 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
||||||
|
|
||||||
// Destroy the scene.
|
// Destroy the scene.
|
||||||
func (s *EditorScene) Destroy() error {
|
func (s *EditorScene) Destroy() error {
|
||||||
debugWorldIndex = render.Origin
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package doodle
|
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.
|
// TODO: build flags to not include this in production builds.
|
||||||
// This adds accessors for private variables from the dev console.
|
// This adds accessors for private variables from the dev console.
|
|
@ -5,15 +5,15 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
|
||||||
"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/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/doodads"
|
||||||
"git.kirsle.net/apps/doodle/uix"
|
"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.
|
// Width of the panel frame.
|
||||||
|
@ -163,11 +163,10 @@ func (u *EditorUI) Loop(ev *events.State) error {
|
||||||
|
|
||||||
// Update status bar labels.
|
// Update status bar labels.
|
||||||
{
|
{
|
||||||
debugWorldIndex = u.Canvas.WorldIndexAt(u.cursor)
|
|
||||||
u.StatusMouseText = fmt.Sprintf("Rel:(%d,%d) Abs:(%s)",
|
u.StatusMouseText = fmt.Sprintf("Rel:(%d,%d) Abs:(%s)",
|
||||||
ev.CursorX.Now,
|
ev.CursorX.Now,
|
||||||
ev.CursorY.Now,
|
ev.CursorY.Now,
|
||||||
debugWorldIndex,
|
*u.Scene.debWorldIndex,
|
||||||
)
|
)
|
||||||
u.StatusPaletteText = fmt.Sprintf("%s Tool",
|
u.StatusPaletteText = fmt.Sprintf("%s Tool",
|
||||||
u.Canvas.Tool,
|
u.Canvas.Tool,
|
|
@ -7,12 +7,12 @@ package doodle
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/doodads"
|
||||||
"git.kirsle.net/apps/doodle/uix"
|
"git.kirsle.net/apps/doodle/src/uix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DraggableActor is a Doodad being dragged from the Doodad palette.
|
// DraggableActor is a Doodad being dragged from the Doodad palette.
|
|
@ -1,9 +1,9 @@
|
||||||
package doodle
|
package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupPaletteFrame configures the Color Palette tab for Edit Mode.
|
// setupPaletteFrame configures the Color Palette tab for Edit Mode.
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/doodads"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frames to cache for FPS calculation.
|
// Frames to cache for FPS calculation.
|
||||||
|
@ -33,37 +33,72 @@ var (
|
||||||
fpsFrames int
|
fpsFrames int
|
||||||
fpsSkipped uint32
|
fpsSkipped uint32
|
||||||
fpsInterval uint32 = 1000
|
fpsInterval uint32 = 1000
|
||||||
|
fpsDoNotCap bool // remove the FPS delay cap in Main Loop
|
||||||
|
|
||||||
// XXX: some opt-in WorldIndex variables for the debug overlay.
|
// Custom labels for scenes to add to the debug overlay view.
|
||||||
// This is the world pixel that the mouse cursor is over,
|
customDebugLabels []debugLabel
|
||||||
// the Cursor + Scroll position of the canvas.
|
|
||||||
debugWorldIndex render.Point
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type debugLabel struct {
|
||||||
|
key string
|
||||||
|
variable *string
|
||||||
|
}
|
||||||
|
|
||||||
// DrawDebugOverlay draws the debug FPS text on the SDL canvas.
|
// DrawDebugOverlay draws the debug FPS text on the SDL canvas.
|
||||||
func (d *Doodle) DrawDebugOverlay() {
|
func (d *Doodle) DrawDebugOverlay() {
|
||||||
if !d.Debug || !DebugOverlay {
|
if !DebugOverlay {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var framesSkipped = fmt.Sprintf("(skip: %dms)", fpsSkipped)
|
||||||
|
if fpsDoNotCap {
|
||||||
|
framesSkipped = "uncapped"
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
darken = balance.DebugStrokeDarken
|
darken = balance.DebugStrokeDarken
|
||||||
Yoffset int32 = 20 // leave room for the menu bar
|
Yoffset int32 = 20 // leave room for the menu bar
|
||||||
Xoffset int32 = 5
|
Xoffset int32 = 20
|
||||||
keys = []string{
|
keys = []string{
|
||||||
"FPS:",
|
"FPS:",
|
||||||
"Scene:",
|
"Scene:",
|
||||||
"Pixel:",
|
|
||||||
"Mouse:",
|
"Mouse:",
|
||||||
}
|
}
|
||||||
values = []string{
|
values = []string{
|
||||||
fmt.Sprintf("%d (skip: %dms)", fpsCurrent, fpsSkipped),
|
fmt.Sprintf("%d %s", fpsCurrent, framesSkipped),
|
||||||
d.Scene.Name(),
|
d.Scene.Name(),
|
||||||
debugWorldIndex.String(),
|
|
||||||
fmt.Sprintf("%d,%d", d.event.CursorX.Now, d.event.CursorY.Now),
|
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, "<nil>")
|
||||||
|
} 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{
|
key := ui.NewLabel(ui.Label{
|
||||||
Text: strings.Join(keys, "\n"),
|
Text: strings.Join(keys, "\n"),
|
||||||
Font: render.Text{
|
Font: render.Text{
|
||||||
|
@ -97,7 +132,7 @@ func (d *Doodle) DrawDebugOverlay() {
|
||||||
|
|
||||||
// DrawCollisionBox draws the collision box around a Doodad.
|
// DrawCollisionBox draws the collision box around a Doodad.
|
||||||
func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
|
func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
|
||||||
if !d.Debug || !DebugCollision {
|
if !DebugCollision {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ package doodle
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GUITestScene implements the main menu of Doodle.
|
// GUITestScene implements the main menu of Doodle.
|
|
@ -1,7 +1,7 @@
|
||||||
package level
|
package level
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
uuid "github.com/satori/go.uuid"
|
uuid "github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"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/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"github.com/satori/go.uuid"
|
"github.com/satori/go.uuid"
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
)
|
)
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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
|
// MapAccessor implements a chunk accessor by using a map of points to their
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test the high level Chunker.
|
// Test the high level Chunker.
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"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
|
// Chunker is the data structure that manages the chunks of a level, and
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWorldSize(t *testing.T) {
|
func TestWorldSize(t *testing.T) {
|
|
@ -28,5 +28,10 @@ const (
|
||||||
// - The wallpaper hoz mirrors Left along the X=Width plane
|
// - The wallpaper hoz mirrors Left along the X=Width plane
|
||||||
// - The wallpaper vert mirrors Top along the Y=Width plane
|
// - The wallpaper vert mirrors Top along the Y=Width plane
|
||||||
// - The wallpaper 180 rotates the Corner for opposite corners
|
// - The wallpaper 180 rotates the Corner for opposite corners
|
||||||
Bordered
|
Bordered // 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
|
||||||
)
|
)
|
|
@ -1,7 +1,7 @@
|
||||||
package level
|
package level
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultPalette returns a sensible default palette.
|
// DefaultPalette returns a sensible default palette.
|
|
@ -3,7 +3,7 @@ package level
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Swatch holds details about a single value in the palette.
|
// Swatch holds details about a single value in the palette.
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Useful variables.
|
// Useful variables.
|
|
@ -1,10 +1,10 @@
|
||||||
package doodle
|
package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MainScene implements the main menu of Doodle.
|
// MainScene implements the main menu of Doodle.
|
|
@ -2,13 +2,15 @@ package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/src/doodads"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/src/doodads/dummy"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
"git.kirsle.net/apps/doodle/uix"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
|
"git.kirsle.net/apps/doodle/src/uix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PlayScene manages the "Edit Level" game mode.
|
// PlayScene manages the "Edit Level" game mode.
|
||||||
|
@ -21,8 +23,14 @@ type PlayScene struct {
|
||||||
d *Doodle
|
d *Doodle
|
||||||
drawing *uix.Canvas
|
drawing *uix.Canvas
|
||||||
|
|
||||||
|
// Custom debug labels.
|
||||||
|
debPosition *string
|
||||||
|
debViewport *string
|
||||||
|
debScroll *string
|
||||||
|
debWorldIndex *string
|
||||||
|
|
||||||
// Player character
|
// Player character
|
||||||
Player doodads.Actor
|
Player *uix.Actor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name of the scene.
|
// Name of the scene.
|
||||||
|
@ -33,6 +41,20 @@ func (s *PlayScene) Name() string {
|
||||||
// Setup the play scene.
|
// Setup the play scene.
|
||||||
func (s *PlayScene) Setup(d *Doodle) error {
|
func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
s.d = d
|
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 = uix.NewCanvas(balance.ChunkSize, false)
|
||||||
s.drawing.MoveTo(render.Origin)
|
s.drawing.MoveTo(render.Origin)
|
||||||
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
|
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?
|
// Given a filename or map data to play?
|
||||||
if s.Level != nil {
|
if s.Level != nil {
|
||||||
log.Debug("PlayScene.Setup: received level from scene caller")
|
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 != "" {
|
} else if s.Filename != "" {
|
||||||
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
|
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
|
||||||
s.LoadLevel(s.Filename)
|
s.LoadLevel(s.Filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Player = doodads.NewPlayer()
|
|
||||||
|
|
||||||
if s.Level == nil {
|
if s.Level == nil {
|
||||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
||||||
s.Level = level.New()
|
s.Level = level.New()
|
||||||
s.drawing.LoadLevel(d.Engine, s.Level)
|
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.")
|
d.Flash("Entered Play Mode. Press 'E' to edit this map.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -62,6 +90,23 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
|
|
||||||
// Loop the editor scene.
|
// Loop the editor scene.
|
||||||
func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
|
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?
|
// Switching to Edit Mode?
|
||||||
if ev.KeyName.Read() == "e" {
|
if ev.KeyName.Read() == "e" {
|
||||||
log.Info("Edit Mode, Go!")
|
log.Info("Edit Mode, Go!")
|
||||||
|
@ -72,8 +117,11 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// s.drawing.Loop(ev)
|
|
||||||
s.movePlayer(ev)
|
s.movePlayer(ev)
|
||||||
|
if err := s.drawing.Loop(ev); err != nil {
|
||||||
|
log.Error("Drawing Loop: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,9 +134,16 @@ func (s *PlayScene) Draw(d *Doodle) error {
|
||||||
s.drawing.Present(d.Engine, s.drawing.Point())
|
s.drawing.Present(d.Engine, s.drawing.Point())
|
||||||
|
|
||||||
// Draw our hero.
|
// 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)
|
d.DrawCollisionBox(s.Player)
|
||||||
|
|
||||||
return nil
|
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.
|
// movePlayer updates the player's X,Y coordinate based on key pressed.
|
||||||
func (s *PlayScene) movePlayer(ev *events.State) {
|
func (s *PlayScene) movePlayer(ev *events.State) {
|
||||||
delta := s.Player.Position()
|
delta := s.Player.Position()
|
||||||
var playerSpeed int32 = 8
|
var playerSpeed = int32(balance.PlayerMaxVelocity)
|
||||||
var gravity int32 = 2
|
var gravity = int32(balance.Gravity)
|
||||||
|
|
||||||
|
var velocity render.Point
|
||||||
|
|
||||||
if ev.Down.Now {
|
if ev.Down.Now {
|
||||||
delta.Y += playerSpeed
|
velocity.Y = playerSpeed
|
||||||
}
|
}
|
||||||
if ev.Left.Now {
|
if ev.Left.Now {
|
||||||
delta.X -= playerSpeed
|
velocity.X = -playerSpeed
|
||||||
}
|
}
|
||||||
if ev.Right.Now {
|
if ev.Right.Now {
|
||||||
delta.X += playerSpeed
|
velocity.X = playerSpeed
|
||||||
}
|
}
|
||||||
if ev.Up.Now {
|
if ev.Up.Now {
|
||||||
delta.Y -= playerSpeed
|
velocity.Y = -playerSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply gravity.
|
// Apply gravity.
|
||||||
// var onFloor bool
|
// 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 {
|
if ok {
|
||||||
// Collision happened with world.
|
// Collision happened with world.
|
||||||
}
|
}
|
||||||
delta = info.MoveTo
|
_ = info.MoveTo
|
||||||
|
|
||||||
// Apply gravity if not grounded.
|
// Apply gravity if not grounded.
|
||||||
if !s.Player.Grounded() {
|
if !s.Player.Grounded() {
|
||||||
// Gravity has to pipe through the collision checker, too, so it
|
// Gravity has to pipe through the collision checker, too, so it
|
||||||
// can't give us a cheated downward boost.
|
// 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.
|
// LoadLevel loads a level from disk.
|
||||||
|
@ -143,6 +208,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
||||||
|
|
||||||
s.Level = level
|
s.Level = level
|
||||||
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
||||||
|
s.drawing.InstallActors(s.Level.Actors)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package doodle
|
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 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
|
// 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.
|
// Goto a scene. First it unloads the current scene.
|
||||||
func (d *Doodle) Goto(scene Scene) error {
|
func (d *Doodle) Goto(scene Scene) error {
|
||||||
|
// Reset any custom debug overlay entries.
|
||||||
|
customDebugLabels = []debugLabel{}
|
||||||
|
|
||||||
// Teardown existing scene.
|
// Teardown existing scene.
|
||||||
if d.Scene != nil {
|
if d.Scene != nil {
|
||||||
d.Scene.Destroy()
|
d.Scene.Destroy()
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/balance"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
"github.com/robertkrimen/otto"
|
"github.com/robertkrimen/otto"
|
||||||
)
|
)
|
||||||
|
|
48
src/uix/actor.go
Normal file
48
src/uix/actor.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
289
src/uix/canvas.go
Normal file
289
src/uix/canvas.go
Normal file
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
115
src/uix/canvas_actors.go
Normal file
115
src/uix/canvas_actors.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package uix
|
package uix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/events"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loopEditable handles the Loop() part for editable canvases.
|
// loopEditable handles the Loop() part for editable canvases.
|
171
src/uix/canvas_present.go
Normal file
171
src/uix/canvas_present.go
Normal file
|
@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
187
src/uix/canvas_scrolling.go
Normal file
187
src/uix/canvas_scrolling.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package uix
|
package uix
|
||||||
|
|
||||||
import (
|
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/pkg/wallpaper"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/src/level"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wallpaper configures the wallpaper in a Canvas.
|
// Wallpaper configures the wallpaper in a Canvas.
|
559
uix/canvas.go
559
uix/canvas.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user