Colliding Doodads #1

Closed
kirsle wants to merge 6 commits from dev into master
99 changed files with 1443 additions and 902 deletions

View File

@ -222,11 +222,14 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
* The texture file will be a square (rectangular maybe ok) with four quadrants
from which the textures will be extracted. For example if the overall image
size was 100x100 pixels, it will be divided into the four 50x50 quadrants.
1. `TL`: Top left corner is the top left edge of the "page" the level is on
2. `TR`: Top right corner is the repeated "top of page" texture.
3. `BL`: Bottom left corner is the repeated "left of page" texture.
4. `BR`: Bottom right corner is the repeated background texture that extends
1. `Corner`: Top left corner is the top left edge of the "page" the level is on
2. `Top`: Top right corner is the repeated "top of page" texture.
3. `Left`: Bottom left corner is the repeated "left of page" texture.
4. `Repeat`: Bottom right corner is the repeated background texture that extends
infinitely in all directions.
* The Repeat texture is used all the time, and the other three are used when the
level type has boundaries (on the top and left edges in particular) to draw
decorative borders instead of the Repeat texture.
* Levels will be able to choose a "page type" which controls how the wallpaper
will be drawn and how the level boundaries may be constrained. There will be
four options:
@ -241,22 +244,14 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
wall.
3. **Bounded:** The map has a fixed width and height and is bounded on all
four edges.
4. **Bounded, Mirrored Wallpaper:** same as Bounded but with a different
wallpaper behavior.
* The page types will have their own behaviors with how wallpapers are drawn:
* **Unbounded:** only the `BR` texture from the wallpaper is used, repeated
infinitely in the X and Y directions. The top-left, top, and left edge
textures are not used.
* **No Negative Space:** the `TL` texture is drawn at coordinate `(0,0)`.
To its right, the `TR` texture is repeated forever in the X direction, and
along the left edge of the page, the `BL` texture is repeated in the Y
direction. The remaining whitespace on the page repeats the `BR` texture
infinitely.
* **Bounded:** same as No Negative Space.
* **Bounded, Mirrored Wallpaper:** same as No Negative Space, but all of the
_other_ corners and edges are textured too, with mirror images of the Top,
Top Left, and Left textures. This would look silly on the "ruled notebook"
texture, but could be useful to emborder the level with a fancy texture.
4. **Bordered:** same as Bounded but the right and bottom edges are decorated
using mirrors of the top and left textures. This would look silly on notebook
paper (having an extra red line and blank margin) but would work for
wrap-around textures like paper placemats.
5. **Full Page:** special level type that treats the wallpaper image as a
literal full page scan to be drawn on top of. The level is bounded by the
literal dimensions of the wallpaper image which is drawn once instead of
cut up and tiled as per the usual behavior.
* The game will come with a few built-in textures for levels to refer to by
name. These textures don't need to be distributed with the map files themselves,
as every copy of the game should include these (or a sensible fallback would
@ -264,6 +259,24 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
* The map author can also attach their own custom texture that will be included
inside the map file.
### Default Wallpapers
**notebook**: standard ruled notebook paper with a red line alone the Left
dge and a blank margin along the Top, with a Corner and the blue lines
aking up the Repeat in all directions.
![notebook.png](../assets/wallpapers/notebook.png)
**graph**: graph paper made up of a grid of light grey or blue lines.
**dots**: graph paper made of dots at the intersections but not the lines in
between.
**legal**: yellow lined notebook paper (legal pad).
**placemat**: a placemat texture with a wavy outline that emborders the map
on all four sides. To be used with the Bordered level type.
# Text Console
* Create a rudimentary dev console for entering text commands in-game. It

View File

@ -2,10 +2,11 @@ SHELL := /bin/bash
VERSION=$(shell grep -e 'Version' doodle.go | head -n 1 | cut -d '"' -f 2)
BUILD=$(shell git describe --always)
BUILD_DATE=$(shell date -Iseconds)
CURDIR=$(shell curdir)
# Inject the build version (commit hash) into the executable.
LDFLAGS := -ldflags "-X main.Build=$(BUILD)"
LDFLAGS := -ldflags "-X main.Build=$(BUILD) -X main.BuildDate=$(BUILD_DATE)"
# `make setup` to set up a new environment, pull dependencies, etc.
.PHONY: setup

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

View File

@ -11,10 +11,10 @@ import (
"image/png"
"git.kirsle.net/apps/doodle"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src"
"git.kirsle.net/apps/doodle/src/doodads"
"git.kirsle.net/apps/doodle/src/level"
"github.com/urfave/cli"
"golang.org/x/image/bmp"
)

View File

@ -2,22 +2,38 @@
package main
import (
"fmt"
"log"
"os"
"sort"
"time"
"git.kirsle.net/apps/doodle"
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
"git.kirsle.net/apps/doodle/src"
"github.com/urfave/cli"
)
var Build = "N/A"
// Build number is the git commit hash.
var (
Build = "<dynamic>"
BuildDate string
)
func init() {
if BuildDate == "" {
BuildDate = time.Now().Format(time.RFC3339)
}
}
func main() {
app := cli.NewApp()
app.Name = "doodad"
app.Usage = "command line interface for Doodle"
app.Version = doodle.Version + " build " + Build
app.Version = fmt.Sprintf("%s build %s. Built on %s",
doodle.Version,
Build,
BuildDate,
)
app.Flags = []cli.Flag{
cli.BoolFlag{

View File

@ -1,59 +1,101 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"sort"
"time"
"git.kirsle.net/apps/doodle"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/render/sdl"
"git.kirsle.net/apps/doodle/lib/render/sdl"
"git.kirsle.net/apps/doodle/src"
"git.kirsle.net/apps/doodle/src/balance"
"github.com/urfave/cli"
_ "image/png"
)
// Build number is the git commit hash.
var Build string
// Command line args
var (
debug bool
edit bool
guitest bool
Build = "<dynamic>"
BuildDate string
)
func init() {
flag.BoolVar(&debug, "debug", false, "Debug mode")
flag.BoolVar(&edit, "edit", false, "Edit the map given on the command line. Default is to play the map.")
flag.BoolVar(&guitest, "guitest", false, "Enter the GUI Test scene.")
if BuildDate == "" {
BuildDate = time.Now().Format(time.RFC3339)
}
}
func main() {
runtime.LockOSThread()
flag.Parse()
args := flag.Args()
var filename string
if len(args) > 0 {
filename = args[0]
}
// SDL engine.
engine := sdl.New(
"Doodle v"+doodle.Version,
balance.Width,
balance.Height,
app := cli.NewApp()
app.Name = "doodle"
app.Usage = "command line interface for Doodle"
app.Version = fmt.Sprintf("%s build %s. Built on %s",
doodle.Version,
Build,
BuildDate,
)
app := doodle.New(debug, engine)
app.SetupEngine()
if guitest {
app.Goto(&doodle.GUITestScene{})
} else if filename != "" {
if edit {
app.EditFile(filename)
} else {
app.PlayLevel(filename)
}
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug, d",
Usage: "enable debug level logging",
},
cli.BoolFlag{
Name: "edit, e",
Usage: "edit the map given on the command line (instead of play it)",
},
cli.BoolFlag{
Name: "guitest",
Usage: "enter the GUI Test scene on startup",
},
}
app.Action = func(c *cli.Context) error {
if c.Bool("version") {
fmt.Printf("This is Project Doodle, v%s build %s ",
doodle.Version,
Build,
)
return nil
}
var filename string
if c.NArg() > 0 {
filename = c.Args().Get(0)
}
// SDL engine.
engine := sdl.New(
"Doodle v"+doodle.Version,
balance.Width,
balance.Height,
)
game := doodle.New(c.Bool("debug"), engine)
game.SetupEngine()
if c.Bool("guitest") {
game.Goto(&doodle.GUITestScene{})
} else if filename != "" {
if c.Bool("edit") {
game.EditFile(filename)
} else {
game.PlayLevel(filename)
}
}
game.Run()
return nil
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
app.Run()
}

View File

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

View File

@ -156,15 +156,15 @@ func (c *Color) UnmarshalJSON(b []byte) error {
}
// Add a relative color value to the color.
func (c Color) Add(r, g, b, a int32) Color {
func (c Color) Add(r, g, b, a int) Color {
var (
R = int32(c.Red) + r
G = int32(c.Green) + g
B = int32(c.Blue) + b
A = int32(c.Alpha) + a
R = int(c.Red) + r
G = int(c.Green) + g
B = int(c.Blue) + b
A = int(c.Alpha) + a
)
cap8 := func(v int32) uint8 {
cap8 := func(v int) uint8 {
if v > 255 {
v = 255
} else if v < 0 {
@ -182,11 +182,22 @@ func (c Color) Add(r, g, b, a int32) Color {
}
// Lighten a color value.
func (c Color) Lighten(v int32) Color {
func (c Color) Lighten(v int) Color {
return c.Add(v, v, v, 0)
}
// Darken a color value.
func (c Color) Darken(v int32) Color {
func (c Color) Darken(v int) Color {
return c.Add(-v, -v, -v, 0)
}
// Transparentize adjusts the alpha 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
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"math"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/src/events"
)
// Engine is the interface for the rendering engine, keeping SDL-specific stuff
@ -125,7 +125,8 @@ func (r Rect) Add(other Rect) Rect {
}
}
// Add a point to move the rect.
// Add (or subtract) a point to move the rect. This is usually the method you
// want to use: negative Point values will subtract the rect's position.
func (r Rect) AddPoint(other Point) Rect {
return Rect{
X: r.X + other.X,
@ -135,6 +136,17 @@ func (r Rect) AddPoint(other Point) Rect {
}
}
// SubtractPoint is the inverse of AddPoint. Use this only if you need to
// invert the Point being added.
func (r Rect) SubtractPoint(other Point) Rect {
return Rect{
X: r.X - other.X,
Y: r.Y - other.Y,
W: r.W,
H: r.H,
}
}
// Text holds information for drawing text.
type Text struct {
Text string

View File

@ -69,12 +69,21 @@ func (p Point) Inside(r Rect) bool {
(p.Y >= y1 && p.Y <= y2))
}
// Add (or subtract) the other point to your current point.
// Add (or subtract) the other point to your current point. This is usually
// the one you want: if the other Point has negative values it will subtract
// them from this Point, or if they are positive it will add them.
func (p *Point) Add(other Point) {
p.X += other.X
p.Y += other.Y
}
// Subtract is the inverse of Add. Use this if you want to force a subtraction
// operation (i.e. to invert a Point before adding it).
func (p *Point) Subtract(other Point) {
p.X -= other.X
p.Y -= other.Y
}
// MarshalText to convert the point into text so that a render.Point may be used
// as a map key and serialized to JSON.
func (p *Point) MarshalText() ([]byte, error) {

View File

@ -4,7 +4,7 @@ import (
"strconv"
"testing"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
func TestPointInside(t *testing.T) {

View File

@ -4,7 +4,7 @@ import (
"strconv"
"testing"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
func TestIntersection(t *testing.T) {

View File

@ -2,7 +2,7 @@
package sdl
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"github.com/veandco/go-sdl2/sdl"
)

View File

@ -3,7 +3,7 @@ package sdl
import (
"errors"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/src/events" // XXX: break it up
"github.com/veandco/go-sdl2/sdl"
)

View File

@ -4,8 +4,8 @@ package sdl
import (
"time"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/events"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
)

View File

@ -4,8 +4,8 @@ import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/events"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
)

View File

@ -3,7 +3,7 @@ package sdl
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"github.com/veandco/go-sdl2/sdl"
)

View File

@ -1,7 +1,7 @@
package sdl
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"github.com/veandco/go-sdl2/sdl"
)

8
lib/ui/README.md Normal file
View 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.

View File

@ -4,8 +4,8 @@ import (
"errors"
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui/theme"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui/theme"
)
// Button is a clickable button.

View File

@ -4,8 +4,8 @@ import (
"fmt"
"strconv"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui/theme"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui/theme"
)
// CheckButton implements a checkbox and radiobox widget. It's based on a

View File

@ -1,6 +1,6 @@
package ui
import "git.kirsle.net/apps/doodle/render"
import "git.kirsle.net/apps/doodle/lib/render"
// Checkbox combines a CheckButton with a widget like a Label.
type Checkbox struct {

View File

@ -3,7 +3,7 @@ package ui
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Frame is a widget that contains other widgets.

View File

@ -1,6 +1,8 @@
package ui
import "git.kirsle.net/apps/doodle/render"
import (
"git.kirsle.net/apps/doodle/lib/render"
)
// Pack provides configuration fields for Frame.Pack().
type Pack struct {

View File

@ -1,6 +1,6 @@
package ui
import "git.kirsle.net/apps/doodle/render"
import "git.kirsle.net/apps/doodle/lib/render"
// AbsolutePosition computes a widget's absolute X,Y position on the
// window on screen by crawling its parent widget tree.

View File

@ -5,7 +5,7 @@ import (
"path/filepath"
"strings"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// ImageType for supported image formats.

View File

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// DefaultFont is the default font settings used for a Label.

View File

@ -4,8 +4,8 @@ import (
"errors"
"sync"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/events"
)
// Event is a named event that the supervisor will send.

View File

@ -1,6 +1,6 @@
package theme
import "git.kirsle.net/apps/doodle/render"
import "git.kirsle.net/apps/doodle/lib/render"
// Color schemes.
var (
@ -8,5 +8,5 @@ var (
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
ButtonOutlineColor = render.Black
BorderColorOffset int32 = 40
BorderColorOffset = 40
)

View File

@ -1,8 +1,8 @@
package ui
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui/theme"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui/theme"
)
// BorderStyle options for widget.SetBorderStyle()

View File

@ -3,7 +3,7 @@ package ui
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Window is a frame with a title bar.

View File

@ -7,8 +7,8 @@ import (
"image"
"os"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"golang.org/x/image/bmp"
)

View File

@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Wallpaper is a repeatable background image to go behind levels.

View File

@ -28,7 +28,7 @@ func TestWallpaper(t *testing.T) {
img := image.NewRGBA(image.Rect(0, 0, width, height))
draw.Draw(
// Corner: red
img, // dst Image
img, // dst Image
image.Rect(0, 0, qWidth, qHeight), // r Rectangle
image.NewUniform(red), // src Image
image.Point{0, 0}, // sp Point

View File

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

View File

@ -5,7 +5,7 @@ import (
"strconv"
"strings"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Debug related variables that can toggle on or off certain features and
@ -16,11 +16,11 @@ var (
***************/
// Debug overlay (FPS etc.) settings.
DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf"
DebugFontSize = 15
DebugLabelColor = render.MustHexColor("#FF9900")
DebugValueColor = render.MustHexColor("#00CCFF")
DebugStrokeDarken int32 = 80
DebugFontFilename = "./fonts/DejaVuSans.ttf"
DebugFontSize = 16
DebugLabelColor = render.MustHexColor("#FF9900")
DebugValueColor = render.MustHexColor("#00CCFF")
DebugStrokeDarken = 80
// Background color to use when exporting a drawing Chunk as a bitmap image
// on disk. Default is white. Setting this to translucent yellow is a great

View File

@ -9,6 +9,16 @@ var (
// Speed to scroll a canvas with arrow keys in Edit Mode.
CanvasScrollSpeed int32 = 8
// Window scrolling behavior in Play Mode.
ScrollboxHoz = 64 // horizontal pixels close to canvas border
ScrollboxVert = 128
ScrollMaxVelocity = 24
// Player speeds
PlayerMaxVelocity = 12
PlayerAcceleration = 2 // pixels / second / second
Gravity = 2
// Default chunk size for canvases.
ChunkSize = 128

View File

@ -1,7 +1,7 @@
package balance
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Shell related variables.

View File

@ -1,8 +1,8 @@
package balance
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
)
// Theme and appearance variables.

View File

@ -5,7 +5,7 @@ import (
"fmt"
"strconv"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/src/enum"
)
// Command is a parsed shell command.
@ -22,6 +22,17 @@ func (c Command) Run(d *Doodle) error {
return nil
}
// Cheats :P
if c.Raw == "unleash the beast" {
if fpsDoNotCap {
d.Flash("Reset frame rate throttle to factory default %d FPS", TargetFPS)
} else {
d.Flash("Unleashing as many frames as we can render!")
}
fpsDoNotCap = !fpsDoNotCap
return nil
}
switch c.Command {
case "echo":
d.Flash(c.ArgsLiteral)

View File

@ -1,8 +1,8 @@
package doodads
import (
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/level"
)
// Actor is a reusable run-time drawing component used in Doodle. Actors are an
@ -13,6 +13,9 @@ type Actor interface {
// Position and velocity, not saved to disk.
Position() render.Point
Velocity() render.Point
SetVelocity(render.Point)
Acceleration() int
SetAcceleration(int)
Size() render.Rect
Grounded() bool
SetGrounded(bool)
@ -20,9 +23,6 @@ type Actor interface {
// Movement commands.
MoveBy(render.Point) // Add {X,Y} to current Position.
MoveTo(render.Point) // Set current Position to {X,Y}.
// Implement the Draw function.
Draw(render.Engine)
}
// GetBoundingRect computes the full pairs of points for the collision box

View File

@ -1,8 +1,8 @@
package doodads
import (
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/level"
)
// Collide describes how a collision occurred.
@ -96,8 +96,15 @@ const (
Right
)
// CollidesWithGrid checks if a Doodad collides with level geometry.
func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) {
/*
CollidesWithGrid checks if a Doodad collides with level geometry.
The `target` is the point the actor wants to move to this tick.
The `scroll` is the scroll offset of the canvas that is viewing the grid, so
that collision geometry can be offset properly on screen.
*/
func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point, scroll render.Point) (*Collide, bool) {
var (
P = d.Position()
S = d.Size()
@ -109,14 +116,14 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
capHeight int32 // Stop vertical movement thru a ceiling
capLeft int32 // Stop movement thru a wall
capRight int32
hitLeft bool // Has hit an obstacle on the left
hitRight bool // or right
hitFloor bool
capFloor int32
capFloor int32 // Stop movement thru the floor
hitLeft bool // Has hit an obstacle on the left
hitRight bool // or right
hitFloor bool // or floor
)
// Test all of the bounding boxes for a collision with level geometry.
if ok := result.ScanBoundingBox(GetBoundingRect(d), grid); ok {
if ok := result.ScanBoundingBox(GetBoundingRect(d).SubtractPoint(scroll), grid); ok {
// We've already collided! Try to wiggle free.
if result.Bottom {
if !d.Grounded() {
@ -190,7 +197,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
Y: point.Y,
W: S.W,
H: S.H,
}, grid); has {
}.SubtractPoint(scroll), grid); has {
if result.Bottom {
if !hitFloor {
hitFloor = true

View File

@ -1,9 +1,9 @@
package doodads
import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/level"
)
// Doodad is a reusable component for Levels that have scripts and graphics.

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

@ -0,0 +1,91 @@
package doodads
import (
"git.kirsle.net/apps/doodle/lib/render"
uuid "github.com/satori/go.uuid"
)
// Drawing is a Doodad Actor that is based on drawings made inside the game.
type Drawing struct {
Doodad *Doodad
id string
point render.Point
velocity render.Point
accel int
size render.Rect
grounded bool
}
// NewDrawing creates a Drawing actor based on a Doodad drawing. If you pass
// an empty ID string, it will make a random UUIDv4 ID.
func NewDrawing(id string, doodad *Doodad) Drawing {
if id == "" {
id = uuid.Must(uuid.NewV4()).String()
}
return Drawing{
id: id,
Doodad: doodad,
size: doodad.Rect(),
}
}
// ID to get the Drawing ID.
func (d *Drawing) ID() string {
return d.id
}
// Position returns the Drawing's position.
func (d *Drawing) Position() render.Point {
return d.point
}
// Velocity returns the Drawing's velocity.
func (d *Drawing) Velocity() render.Point {
return d.velocity
}
// SetVelocity to set the speed.
func (d *Drawing) SetVelocity(v render.Point) {
d.velocity = v
}
// Acceleration returns the Drawing's acceleration.
func (d *Drawing) Acceleration() int {
return d.accel
}
// SetAcceleration to set the acceleration.
func (d *Drawing) SetAcceleration(v int) {
d.accel = v
}
// Size returns the Drawing's size.
func (d *Drawing) Size() render.Rect {
return d.size
}
// Grounded returns whether the Drawing is standing on solid ground.
func (d *Drawing) Grounded() bool {
return d.grounded
}
// SetGrounded sets the grounded state.
func (d *Drawing) SetGrounded(v bool) {
d.grounded = v
}
// MoveBy a relative value.
func (d *Drawing) MoveBy(by render.Point) {
d.point.Add(by)
}
// MoveTo an absolute world value.
func (d *Drawing) MoveTo(to render.Point) {
d.point = to
}
// Draw the drawing.
func (d *Drawing) Draw(e render.Engine) {
}

View File

@ -0,0 +1,16 @@
// Package dummy implements a dummy doodads.Drawing.
package dummy
import "git.kirsle.net/apps/doodle/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),
}
}

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

View File

@ -5,16 +5,17 @@ import (
"strings"
"time"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/enum"
"git.kirsle.net/apps/doodle/src/events"
"github.com/kirsle/golog"
)
const (
// Version number.
Version = "0.0.1-alpha"
Version = "0.0.7-alpha"
// TargetFPS is the frame rate to cap the game to.
TargetFPS = 1000 / 60 // 60 FPS
@ -147,13 +148,15 @@ func (d *Doodle) Run() error {
}
// Delay to maintain the target frames per second.
elapsed := time.Now().Sub(start)
tmp := elapsed / time.Millisecond
var delay uint32
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under
delay = uint32(TargetFPS - int(tmp))
if !fpsDoNotCap {
elapsed := time.Now().Sub(start)
tmp := elapsed / time.Millisecond
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under
delay = uint32(TargetFPS - int(tmp))
}
d.Engine.Delay(delay)
}
d.Engine.Delay(delay)
// Track how long this frame took to measure FPS over time.
d.TrackFPS(delay)
@ -216,7 +219,7 @@ func (d *Doodle) EditDrawing(filename string) error {
func (d *Doodle) PlayLevel(filename string) error {
log.Info("Loading level from file: %s", filename)
scene := &PlayScene{
Filename: filename,
Filename: userdir.LevelPath(filename),
}
d.Goto(scene)
return nil

View File

@ -3,7 +3,7 @@ package draw
import (
"math"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Line is a generator that returns the X,Y coordinates to draw a line.

View File

@ -5,7 +5,7 @@ import (
"testing"
"git.kirsle.net/apps/doodle/draw"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
func TestLine(t *testing.T) {

View File

@ -7,12 +7,12 @@ import (
"os"
"strings"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/src/doodads"
"git.kirsle.net/apps/doodle/src/enum"
"git.kirsle.net/apps/doodle/src/events"
"git.kirsle.net/apps/doodle/src/level"
)
// EditorScene manages the "Edit Level" game mode.
@ -31,6 +31,11 @@ type EditorScene struct {
Level *level.Level
Doodad *doodads.Doodad
// Custom debug overlay labels.
debTool *string
debSwatch *string
debWorldIndex *string
// Last saved filename by the user.
filename string
}
@ -42,6 +47,16 @@ func (s *EditorScene) Name() string {
// Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error {
// Debug overlay labels.
s.debTool = new(string)
s.debSwatch = new(string)
s.debWorldIndex = new(string)
customDebugLabels = []debugLabel{
{"Pixel:", s.debWorldIndex},
{"Tool:", s.debTool},
{"Swatch:", s.debSwatch},
}
// Initialize the user interface. It references the palette and such so it
// must be initialized after those things.
s.d = d
@ -60,6 +75,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller")
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.InstallActors(s.Level.Actors)
} else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
if err := s.LoadLevel(s.filename); err != nil {
@ -106,6 +122,11 @@ func (s *EditorScene) Setup(d *Doodle) error {
// Loop the editor scene.
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
// Update debug overlay labels.
*s.debTool = s.UI.Canvas.Tool.String()
*s.debSwatch = s.UI.Canvas.Palette.ActiveSwatch.Name
*s.debWorldIndex = s.UI.Canvas.WorldIndexAt(s.UI.cursor).String()
// Has the window been resized?
if resized := ev.Resized.Read(); resized {
w, h := d.Engine.WindowSize()
@ -261,6 +282,5 @@ func (s *EditorScene) SaveDoodad(filename string) error {
// Destroy the scene.
func (s *EditorScene) Destroy() error {
debugWorldIndex = render.Origin
return nil
}

View File

@ -1,6 +1,6 @@
package doodle
import "git.kirsle.net/apps/doodle/uix"
import "git.kirsle.net/apps/doodle/src/uix"
// TODO: build flags to not include this in production builds.
// This adds accessors for private variables from the dev console.

View File

@ -5,15 +5,15 @@ import (
"path/filepath"
"strconv"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/uix"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/doodads"
"git.kirsle.net/apps/doodle/src/enum"
"git.kirsle.net/apps/doodle/src/events"
"git.kirsle.net/apps/doodle/src/level"
"git.kirsle.net/apps/doodle/src/uix"
)
// Width of the panel frame.
@ -163,11 +163,10 @@ func (u *EditorUI) Loop(ev *events.State) error {
// Update status bar labels.
{
debugWorldIndex = u.Canvas.WorldIndexAt(u.cursor)
u.StatusMouseText = fmt.Sprintf("Rel:(%d,%d) Abs:(%s)",
ev.CursorX.Now,
ev.CursorY.Now,
debugWorldIndex,
*u.Scene.debWorldIndex,
)
u.StatusPaletteText = fmt.Sprintf("%s Tool",
u.Canvas.Tool,

View File

@ -7,12 +7,12 @@ package doodle
import (
"fmt"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/uix"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/doodads"
"git.kirsle.net/apps/doodle/src/uix"
)
// DraggableActor is a Doodad being dragged from the Doodad palette.

View File

@ -1,9 +1,9 @@
package doodle
import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/src/balance"
)
// setupPaletteFrame configures the Color Palette tab for Edit Mode.

View File

@ -4,10 +4,10 @@ import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/doodads"
)
// Frames to cache for FPS calculation.
@ -33,37 +33,72 @@ var (
fpsFrames int
fpsSkipped uint32
fpsInterval uint32 = 1000
fpsDoNotCap bool // remove the FPS delay cap in Main Loop
// XXX: some opt-in WorldIndex variables for the debug overlay.
// This is the world pixel that the mouse cursor is over,
// the Cursor + Scroll position of the canvas.
debugWorldIndex render.Point
// Custom labels for scenes to add to the debug overlay view.
customDebugLabels []debugLabel
)
type debugLabel struct {
key string
variable *string
}
// DrawDebugOverlay draws the debug FPS text on the SDL canvas.
func (d *Doodle) DrawDebugOverlay() {
if !d.Debug || !DebugOverlay {
if !DebugOverlay {
return
}
var framesSkipped = fmt.Sprintf("(skip: %dms)", fpsSkipped)
if fpsDoNotCap {
framesSkipped = "uncapped"
}
var (
darken = balance.DebugStrokeDarken
Yoffset int32 = 20 // leave room for the menu bar
Xoffset int32 = 5
Xoffset int32 = 20
keys = []string{
" FPS:",
"FPS:",
"Scene:",
"Pixel:",
"Mouse:",
}
values = []string{
fmt.Sprintf("%d (skip: %dms)", fpsCurrent, fpsSkipped),
fmt.Sprintf("%d %s", fpsCurrent, framesSkipped),
d.Scene.Name(),
debugWorldIndex.String(),
fmt.Sprintf("%d,%d", d.event.CursorX.Now, d.event.CursorY.Now),
}
)
// Insert custom keys.
for _, custom := range customDebugLabels {
keys = append(keys, custom.key)
if custom.variable == nil {
values = append(values, "<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{
Text: strings.Join(keys, "\n"),
Font: render.Text{
@ -97,7 +132,7 @@ func (d *Doodle) DrawDebugOverlay() {
// DrawCollisionBox draws the collision box around a Doodad.
func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
if !d.Debug || !DebugCollision {
if !DebugCollision {
return
}

View File

@ -3,10 +3,10 @@ package doodle
import (
"fmt"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/events"
)
// GUITestScene implements the main menu of Doodle.

View File

@ -1,7 +1,7 @@
package level
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
uuid "github.com/satori/go.uuid"
)

View File

@ -7,9 +7,9 @@ import (
"math"
"os"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/src/balance"
"github.com/satori/go.uuid"
"golang.org/x/image/bmp"
)

View File

@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// MapAccessor implements a chunk accessor by using a map of points to their

View File

@ -5,7 +5,7 @@ import (
"testing"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Test the high level Chunker.

View File

@ -5,7 +5,7 @@ import (
"fmt"
"math"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Chunker is the data structure that manages the chunks of a level, and

View File

@ -5,7 +5,7 @@ import (
"testing"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
func TestWorldSize(t *testing.T) {

View File

@ -28,5 +28,10 @@ const (
// - The wallpaper hoz mirrors Left along the X=Width plane
// - The wallpaper vert mirrors Top along the Y=Width plane
// - The wallpaper 180 rotates the Corner for opposite corners
Bordered
Bordered // TODO: to be implemented
// FullPage treats the wallpaper image as a literal full scan of a page.
// - The level boundaries are fixed to the wallpaper image's dimensions.
// - Used to e.g. scan in real letterhead paper and draw a map on it.
FullPage // TODO: to be implemented
)

View File

@ -1,7 +1,7 @@
package level
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// DefaultPalette returns a sensible default palette.

View File

@ -3,7 +3,7 @@ package level
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
)
// Swatch holds details about a single value in the palette.

View File

@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/balance"
)
// Useful variables.

View File

View File

@ -1,10 +1,10 @@
package doodle
import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/events"
)
// MainScene implements the main menu of Doodle.

View File

@ -2,13 +2,15 @@ package doodle
import (
"fmt"
"math"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/uix"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/doodads"
"git.kirsle.net/apps/doodle/src/doodads/dummy"
"git.kirsle.net/apps/doodle/src/events"
"git.kirsle.net/apps/doodle/src/level"
"git.kirsle.net/apps/doodle/src/uix"
)
// PlayScene manages the "Edit Level" game mode.
@ -21,8 +23,14 @@ type PlayScene struct {
d *Doodle
drawing *uix.Canvas
// Custom debug labels.
debPosition *string
debViewport *string
debScroll *string
debWorldIndex *string
// Player character
Player doodads.Actor
Player *uix.Actor
}
// Name of the scene.
@ -33,6 +41,20 @@ func (s *PlayScene) Name() string {
// Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error {
s.d = d
// Initialize debug bound variables.
s.debPosition = new(string)
s.debViewport = new(string)
s.debScroll = new(string)
s.debWorldIndex = new(string)
customDebugLabels = []debugLabel{
{"Pixel:", s.debWorldIndex},
{"Player:", s.debPosition},
{"Viewport:", s.debViewport},
{"Scroll:", s.debScroll},
}
// Initialize the drawing canvas.
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
s.drawing.MoveTo(render.Origin)
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
@ -41,20 +63,26 @@ func (s *PlayScene) Setup(d *Doodle) error {
// Given a filename or map data to play?
if s.Level != nil {
log.Debug("PlayScene.Setup: received level from scene caller")
s.drawing.LoadLevel(d.Engine, s.Level)
s.drawing.LoadLevel(s.d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors)
} else if s.Filename != "" {
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
s.LoadLevel(s.Filename)
}
s.Player = doodads.NewPlayer()
if s.Level == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
s.Level = level.New()
s.drawing.LoadLevel(d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors)
}
player := dummy.NewPlayer()
s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad)
s.Player.MoveTo(render.NewPoint(128, 128))
s.drawing.AddActor(s.Player)
s.drawing.FollowActor = s.Player.ID()
d.Flash("Entered Play Mode. Press 'E' to edit this map.")
return nil
@ -62,6 +90,23 @@ func (s *PlayScene) Setup(d *Doodle) error {
// Loop the editor scene.
func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
// Update debug overlay variables.
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)).String()
*s.debPosition = s.Player.Position().String()
*s.debViewport = s.drawing.Viewport().String()
*s.debScroll = s.drawing.Scroll.String()
// Has the window been resized?
if resized := ev.Resized.Read(); resized {
w, h := d.Engine.WindowSize()
if w != d.width || h != d.height {
d.width = w
d.height = h
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
return nil
}
}
// Switching to Edit Mode?
if ev.KeyName.Read() == "e" {
log.Info("Edit Mode, Go!")
@ -72,8 +117,11 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
return nil
}
// s.drawing.Loop(ev)
s.movePlayer(ev)
if err := s.drawing.Loop(ev); err != nil {
log.Error("Drawing Loop: %s", err)
}
return nil
}
@ -86,9 +134,16 @@ func (s *PlayScene) Draw(d *Doodle) error {
s.drawing.Present(d.Engine, s.drawing.Point())
// Draw our hero.
s.Player.Draw(d.Engine)
{
d.Engine.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{
X: s.Player.Position().X,
Y: s.Player.Position().Y,
W: s.Player.Size().W,
H: s.Player.Size().H,
})
}
// Draw out bounding boxes.
// Draw our bounding boxes.
d.DrawCollisionBox(s.Player)
return nil
@ -97,39 +152,49 @@ func (s *PlayScene) Draw(d *Doodle) error {
// movePlayer updates the player's X,Y coordinate based on key pressed.
func (s *PlayScene) movePlayer(ev *events.State) {
delta := s.Player.Position()
var playerSpeed int32 = 8
var gravity int32 = 2
var playerSpeed = int32(balance.PlayerMaxVelocity)
var gravity = int32(balance.Gravity)
var velocity render.Point
if ev.Down.Now {
delta.Y += playerSpeed
velocity.Y = playerSpeed
}
if ev.Left.Now {
delta.X -= playerSpeed
velocity.X = -playerSpeed
}
if ev.Right.Now {
delta.X += playerSpeed
velocity.X = playerSpeed
}
if ev.Up.Now {
delta.Y -= playerSpeed
velocity.Y = -playerSpeed
}
// Apply gravity.
// var onFloor bool
info, ok := doodads.CollidesWithGrid(s.Player, s.Level.Chunker, delta)
info, ok := doodads.CollidesWithGrid(s.Player, s.Level.Chunker, delta, s.drawing.Scroll)
if ok {
// Collision happened with world.
}
delta = info.MoveTo
_ = info.MoveTo
// Apply gravity if not grounded.
if !s.Player.Grounded() {
// Gravity has to pipe through the collision checker, too, so it
// can't give us a cheated downward boost.
delta.Y += gravity
velocity.Y += gravity
}
s.Player.MoveTo(delta)
// Cap the max speed.
if int(math.Abs(float64(velocity.X))) > balance.PlayerMaxVelocity {
velocity.X = int32(balance.PlayerMaxVelocity)
}
if int(math.Abs(float64(velocity.Y))) > balance.PlayerMaxVelocity {
velocity.Y = int32(balance.PlayerMaxVelocity)
}
s.Player.SetVelocity(velocity)
}
// LoadLevel loads a level from disk.
@ -143,6 +208,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
s.Level = level
s.drawing.LoadLevel(s.d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors)
return nil
}

View File

@ -1,6 +1,8 @@
package doodle
import "git.kirsle.net/apps/doodle/events"
import (
"git.kirsle.net/apps/doodle/src/events"
)
// Scene is an abstraction for a game mode in Doodle. The app points to one
// scene at a time and that scene has control over the main loop, and its own
@ -20,6 +22,9 @@ type Scene interface {
// Goto a scene. First it unloads the current scene.
func (d *Doodle) Goto(scene Scene) error {
// Reset any custom debug overlay entries.
customDebugLabels = []debugLabel{}
// Teardown existing scene.
if d.Scene != nil {
d.Scene.Destroy()

View File

@ -5,10 +5,10 @@ import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/src/balance"
"git.kirsle.net/apps/doodle/src/events"
"github.com/robertkrimen/otto"
)

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

@ -0,0 +1,48 @@
package uix
import (
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/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
View 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
View 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)
}
}

View File

@ -1,10 +1,10 @@
package uix
import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/src/events"
"git.kirsle.net/apps/doodle/src/level"
)
// loopEditable handles the Loop() part for editable canvases.

171
src/uix/canvas_present.go Normal file
View 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
View 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)
}

View File

@ -1,9 +1,9 @@
package uix
import (
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/wallpaper"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/src/level"
)
// Wallpaper configures the wallpaper in a Canvas.

View File

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