Colliding Doodads #1
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ maps/
|
||||||
bin/
|
bin/
|
||||||
screenshot-*.png
|
screenshot-*.png
|
||||||
map-*.json
|
map-*.json
|
||||||
|
pkg/wallpaper/*.png
|
||||||
|
|
BIN
assets/wallpapers/notebook.png
Normal file
BIN
assets/wallpapers/notebook.png
Normal file
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
@ -35,3 +35,7 @@ like `#FF00FF99` for 153 ($99) on the alpha channel.
|
||||||
* `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows
|
* `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows
|
||||||
in the Editor Mode, in pixels per tick.
|
in the Editor Mode, in pixels per tick.
|
||||||
* `D_DOODAD_SIZE=100`: Default size when creating a new Doodad.
|
* `D_DOODAD_SIZE=100`: Default size when creating a new Doodad.
|
||||||
|
|
||||||
|
Development booleans for unit tests (set to any non-empty value):
|
||||||
|
|
||||||
|
* `T_WALLPAPER_PNG` for pkg/wallpaper to output PNG images.
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"git.kirsle.net/apps/doodle"
|
"git.kirsle.net/apps/doodle"
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
"git.kirsle.net/apps/doodle/render/sdl"
|
"git.kirsle.net/apps/doodle/render/sdl"
|
||||||
|
|
||||||
|
_ "image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build number is the git commit hash.
|
// Build number is the git commit hash.
|
||||||
|
|
66
docs/Doodad Scripts.md
Normal file
66
docs/Doodad Scripts.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Doodad Scripting Engine
|
||||||
|
|
||||||
|
Some ideas for the scripting engine for Doodads inside your level.
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
The script will be an "attached file" in the Doodad format as a special file
|
||||||
|
named "index.js" as the entry point.
|
||||||
|
|
||||||
|
Each Doodad will have its `index.js` script loaded into an isolated JS
|
||||||
|
environment where it can't access any data about other Doodads or anything
|
||||||
|
user specific. The `main()` function is called so the Doodad script can
|
||||||
|
set itself up.
|
||||||
|
|
||||||
|
The `main()` function should:
|
||||||
|
|
||||||
|
* Initialize any state variables the Doodad wants to use in its script.
|
||||||
|
* Subscribe to callback events that the Doodad is interested in catching.
|
||||||
|
|
||||||
|
The script interacts with the Doodle application through an API broker object
|
||||||
|
(a Go surface area of functions).
|
||||||
|
|
||||||
|
# API Broker Interface
|
||||||
|
|
||||||
|
```go
|
||||||
|
type API interface {
|
||||||
|
// "Self" functions.
|
||||||
|
SetFrame(frame int) // Set the currently visible frame in this Doodad.
|
||||||
|
MoveTo(render.Point)
|
||||||
|
|
||||||
|
// Game functions.k
|
||||||
|
EndLevel() // Exit the current level with a victory
|
||||||
|
|
||||||
|
/************************************
|
||||||
|
* Event Handler Callback Functions *
|
||||||
|
************************************/
|
||||||
|
|
||||||
|
// When we become visible on screen or disappear off the screen.
|
||||||
|
OnVisible()
|
||||||
|
OnHidden()
|
||||||
|
|
||||||
|
// OnEnter: the other Doodad has ENTIRELY entered our box. Or if the other
|
||||||
|
// doodad is bigger, they have ENTIRELY enveloped ours.
|
||||||
|
OnEnter(func(other Doodad))
|
||||||
|
|
||||||
|
// OnCollide: when we bump into another Doodad.
|
||||||
|
OnCollide(func(other Doodad))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mockup Script
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function main() {
|
||||||
|
console.log("hello world");
|
||||||
|
|
||||||
|
// Register event callbacks.
|
||||||
|
Doodle.OnEnter(onEnter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// onEnter: handle when another Doodad (like the player) completely enters
|
||||||
|
// the bounding box of our Doodad. Example: a level exit.
|
||||||
|
function onEnter(other) {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
|
@ -24,6 +24,7 @@ type EditorScene struct {
|
||||||
DoodadSize int
|
DoodadSize int
|
||||||
|
|
||||||
UI *EditorUI
|
UI *EditorUI
|
||||||
|
d *Doodle
|
||||||
|
|
||||||
// The current level or doodad object being edited, based on the
|
// The current level or doodad object being edited, based on the
|
||||||
// DrawingType.
|
// DrawingType.
|
||||||
|
@ -43,6 +44,7 @@ func (s *EditorScene) Name() string {
|
||||||
func (s *EditorScene) Setup(d *Doodle) error {
|
func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
// 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.UI = NewEditorUI(d, s)
|
s.UI = NewEditorUI(d, s)
|
||||||
|
|
||||||
// Were we given configuration data?
|
// Were we given configuration data?
|
||||||
|
@ -57,7 +59,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
case enum.LevelDrawing:
|
case enum.LevelDrawing:
|
||||||
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(s.Level)
|
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
|
||||||
s.UI.Canvas.InstallActors(s.Level.Actors)
|
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)
|
||||||
|
@ -71,7 +73,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
log.Debug("EditorScene.Setup: initializing a new Level")
|
log.Debug("EditorScene.Setup: initializing a new Level")
|
||||||
s.Level = level.New()
|
s.Level = level.New()
|
||||||
s.Level.Palette = level.DefaultPalette()
|
s.Level.Palette = level.DefaultPalette()
|
||||||
s.UI.Canvas.LoadLevel(s.Level)
|
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
|
||||||
s.UI.Canvas.ScrollTo(render.Origin)
|
s.UI.Canvas.ScrollTo(render.Origin)
|
||||||
s.UI.Canvas.Scrollable = true
|
s.UI.Canvas.Scrollable = true
|
||||||
}
|
}
|
||||||
|
@ -154,7 +156,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
|
|
||||||
s.DrawingType = enum.LevelDrawing
|
s.DrawingType = enum.LevelDrawing
|
||||||
s.Level = level
|
s.Level = level
|
||||||
s.UI.Canvas.LoadLevel(s.Level)
|
s.UI.Canvas.LoadLevel(s.d.Engine, s.Level)
|
||||||
|
|
||||||
// TODO: debug
|
// TODO: debug
|
||||||
for i, actor := range level.Actors {
|
for i, actor := range level.Actors {
|
||||||
|
|
|
@ -48,6 +48,11 @@ func LoadJSON(filename string) (*Level, error) {
|
||||||
return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err)
|
return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in defaults.
|
||||||
|
if m.Wallpaper == "" {
|
||||||
|
m.Wallpaper = DefaultWallpaper
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
||||||
m.Chunker.Inflate(m.Palette)
|
m.Chunker.Inflate(m.Palette)
|
||||||
m.Actors.Inflate()
|
m.Actors.Inflate()
|
||||||
|
|
32
level/page_type.go
Normal file
32
level/page_type.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
// PageType configures the bounds and wallpaper behavior of a Level.
|
||||||
|
type PageType int
|
||||||
|
|
||||||
|
// PageType values.
|
||||||
|
const (
|
||||||
|
// Unbounded means the map can grow freely in any direction.
|
||||||
|
// - Only the repeat texture is used for the wallpaper.
|
||||||
|
Unbounded PageType = iota
|
||||||
|
|
||||||
|
// NoNegativeSpace means the map is bounded at the top left edges.
|
||||||
|
// - Can't scroll or visit any pixels in negative X,Y coordinates.
|
||||||
|
// - Wallpaper shows the Corner at 0,0
|
||||||
|
// - Wallpaper repeats the Top along the Y=0 plane
|
||||||
|
// - Wallpaper repeats the Left along the X=0 plane
|
||||||
|
// - The repeat texture fills the rest of the level.
|
||||||
|
NoNegativeSpace
|
||||||
|
|
||||||
|
// Bounded is the same as NoNegativeSpace but the level is imposing a
|
||||||
|
// maximum cap on the width and height of the level.
|
||||||
|
// - Can't scroll below X,Y origin at 0,0
|
||||||
|
// - Can't scroll past the bounded width and height of the level
|
||||||
|
Bounded
|
||||||
|
|
||||||
|
// Bordered is like Bounded except the corner textures are wrapped
|
||||||
|
// around the other edges of the level too.
|
||||||
|
// - 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
|
||||||
|
)
|
|
@ -8,6 +8,11 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Useful variables.
|
||||||
|
var (
|
||||||
|
DefaultWallpaper = "notebook.png"
|
||||||
|
)
|
||||||
|
|
||||||
// Base provides the common struct keys that are shared between Levels and
|
// Base provides the common struct keys that are shared between Levels and
|
||||||
// Doodads.
|
// Doodads.
|
||||||
type Base struct {
|
type Base struct {
|
||||||
|
@ -33,6 +38,12 @@ type Level struct {
|
||||||
// properties (solid, fire, slippery, etc.)
|
// properties (solid, fire, slippery, etc.)
|
||||||
Palette *Palette `json:"palette"`
|
Palette *Palette `json:"palette"`
|
||||||
|
|
||||||
|
// Page boundaries and wallpaper settings.
|
||||||
|
PageType PageType `json:"pageType"`
|
||||||
|
MaxWidth int64 `json:"boundedWidth"` // only if bounded or bordered
|
||||||
|
MaxHeight int64 `json:"boundedHeight"`
|
||||||
|
Wallpaper string `json:"wallpaper"`
|
||||||
|
|
||||||
// Actors keep a list of the doodad instances in this map.
|
// Actors keep a list of the doodad instances in this map.
|
||||||
Actors ActorMap `json:"actors"`
|
Actors ActorMap `json:"actors"`
|
||||||
}
|
}
|
||||||
|
@ -46,6 +57,11 @@ func New() *Level {
|
||||||
Chunker: NewChunker(balance.ChunkSize),
|
Chunker: NewChunker(balance.ChunkSize),
|
||||||
Palette: &Palette{},
|
Palette: &Palette{},
|
||||||
Actors: ActorMap{},
|
Actors: ActorMap{},
|
||||||
|
|
||||||
|
PageType: NoNegativeSpace,
|
||||||
|
Wallpaper: DefaultWallpaper,
|
||||||
|
MaxWidth: 2550,
|
||||||
|
MaxHeight: 3300,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
76
pkg/wallpaper/texture.go
Normal file
76
pkg/wallpaper/texture.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package wallpaper
|
||||||
|
|
||||||
|
// The methods that deal in cached Textures for Doodle.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CornerTexture returns the Texture.
|
||||||
|
func (wp *Wallpaper) CornerTexture(e render.Engine) (render.Texturer, error) {
|
||||||
|
fmt.Println("CornerTex")
|
||||||
|
if wp.tex.corner == nil {
|
||||||
|
tex, err := texture(e, wp.corner, wp.Name+"c")
|
||||||
|
wp.tex.corner = tex
|
||||||
|
return tex, err
|
||||||
|
}
|
||||||
|
return wp.tex.corner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopTexture returns the Texture.
|
||||||
|
func (wp *Wallpaper) TopTexture(e render.Engine) (render.Texturer, error) {
|
||||||
|
if wp.tex.top == nil {
|
||||||
|
tex, err := texture(e, wp.top, wp.Name+"t")
|
||||||
|
wp.tex.top = tex
|
||||||
|
return tex, err
|
||||||
|
}
|
||||||
|
return wp.tex.top, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeftTexture returns the Texture.
|
||||||
|
func (wp *Wallpaper) LeftTexture(e render.Engine) (render.Texturer, error) {
|
||||||
|
if wp.tex.left == nil {
|
||||||
|
tex, err := texture(e, wp.left, wp.Name+"l")
|
||||||
|
wp.tex.left = tex
|
||||||
|
return tex, err
|
||||||
|
}
|
||||||
|
return wp.tex.left, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepeatTexture returns the Texture.
|
||||||
|
func (wp *Wallpaper) RepeatTexture(e render.Engine) (render.Texturer, error) {
|
||||||
|
if wp.tex.repeat == nil {
|
||||||
|
tex, err := texture(e, wp.repeat, wp.Name+"x")
|
||||||
|
wp.tex.repeat = tex
|
||||||
|
return tex, err
|
||||||
|
}
|
||||||
|
return wp.tex.repeat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func texture(e render.Engine, img *image.RGBA, name string) (render.Texturer, error) {
|
||||||
|
filename := userdir.CacheFilename("wallpaper", name+".bmp")
|
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
|
fh, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CornerTexture: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
err = bmp.Encode(fh, img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CornerTexture: bmp.Encode: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
texture, err := e.NewBitmap(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CornerTexture: NewBitmap(%s): %s", filename, err.Error())
|
||||||
|
}
|
||||||
|
return texture, nil
|
||||||
|
}
|
143
pkg/wallpaper/wallpaper.go
Normal file
143
pkg/wallpaper/wallpaper.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/draw"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wallpaper is a repeatable background image to go behind levels.
|
||||||
|
type Wallpaper struct {
|
||||||
|
Name string
|
||||||
|
Format string // image file format
|
||||||
|
Image *image.RGBA
|
||||||
|
|
||||||
|
// Parsed values.
|
||||||
|
quarterWidth int
|
||||||
|
quarterHeight int
|
||||||
|
|
||||||
|
// The four parsed images.
|
||||||
|
corner *image.RGBA // Top Left corner
|
||||||
|
top *image.RGBA // Top repeating
|
||||||
|
left *image.RGBA // Left repeating
|
||||||
|
repeat *image.RGBA // Main repeating
|
||||||
|
|
||||||
|
// Cached textures.
|
||||||
|
tex struct {
|
||||||
|
corner render.Texturer
|
||||||
|
top render.Texturer
|
||||||
|
left render.Texturer
|
||||||
|
repeat render.Texturer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromImage creates a Wallpaper from an image.Image.
|
||||||
|
// If the renger.Engine is nil it will compute images but not pre-cache any
|
||||||
|
// textures yet.
|
||||||
|
func FromImage(e render.Engine, img *image.RGBA, name string) (*Wallpaper, error) {
|
||||||
|
wp := &Wallpaper{
|
||||||
|
Name: name,
|
||||||
|
Image: img,
|
||||||
|
}
|
||||||
|
wp.cache(e)
|
||||||
|
return wp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromFile creates a Wallpaper from a file on disk.
|
||||||
|
// If the renger.Engine is nil it will compute images but not pre-cache any
|
||||||
|
// textures yet.
|
||||||
|
func FromFile(e render.Engine, filename string) (*Wallpaper, error) {
|
||||||
|
fh, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
img, format, err := image.Decode(fh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ugly hack: make it an image.RGBA because the thing we get tends to be
|
||||||
|
// an image.Paletted, UGH!
|
||||||
|
var b = img.Bounds()
|
||||||
|
rgba := image.NewRGBA(b)
|
||||||
|
for x := b.Min.X; x < b.Max.X; x++ {
|
||||||
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||||
|
rgba.Set(x, y, img.At(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp := &Wallpaper{
|
||||||
|
Name: strings.Split(filepath.Base(filename), ".")[0],
|
||||||
|
Format: format,
|
||||||
|
Image: rgba,
|
||||||
|
}
|
||||||
|
wp.cache(e)
|
||||||
|
return wp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache the bitmap images.
|
||||||
|
func (wp *Wallpaper) cache(e render.Engine) {
|
||||||
|
// Zero-bound the rect cuz an image.Rect doesn't necessarily contain 0,0
|
||||||
|
var rect = wp.Image.Bounds()
|
||||||
|
if rect.Min.X < 0 {
|
||||||
|
rect.Max.X += rect.Min.X
|
||||||
|
rect.Min.X = 0
|
||||||
|
}
|
||||||
|
if rect.Min.Y < 0 {
|
||||||
|
rect.Max.Y += rect.Min.Y
|
||||||
|
rect.Min.Y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our quarter rect size.
|
||||||
|
wp.quarterWidth = int(float64((rect.Max.X - rect.Min.X) / 2))
|
||||||
|
wp.quarterHeight = int(float64((rect.Max.Y - rect.Min.Y) / 2))
|
||||||
|
quarter := image.Rect(0, 0, wp.quarterWidth, wp.quarterHeight)
|
||||||
|
|
||||||
|
// Slice the image into the four corners.
|
||||||
|
slice := func(dx, dy int) *image.RGBA {
|
||||||
|
slice := image.NewRGBA(quarter)
|
||||||
|
draw.Draw(
|
||||||
|
slice,
|
||||||
|
image.Rect(0, 0, wp.quarterWidth, wp.quarterHeight),
|
||||||
|
wp.Image,
|
||||||
|
image.Point{dx, dy},
|
||||||
|
draw.Over,
|
||||||
|
)
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
wp.corner = slice(0, 0)
|
||||||
|
wp.top = slice(wp.quarterWidth, 0)
|
||||||
|
wp.left = slice(0, wp.quarterHeight)
|
||||||
|
wp.repeat = slice(wp.quarterWidth, wp.quarterHeight)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuarterSize returns the width and height of the quarter images.
|
||||||
|
func (wp *Wallpaper) QuarterSize() (int, int) {
|
||||||
|
return wp.quarterWidth, wp.quarterHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corner returns the top left corner image.
|
||||||
|
func (wp *Wallpaper) Corner() *image.RGBA {
|
||||||
|
return wp.corner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top returns the top repeating image.
|
||||||
|
func (wp *Wallpaper) Top() *image.RGBA {
|
||||||
|
return wp.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left returns the left repeating image.
|
||||||
|
func (wp *Wallpaper) Left() *image.RGBA {
|
||||||
|
return wp.left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat returns the main repeating image.
|
||||||
|
func (wp *Wallpaper) Repeat() *image.RGBA {
|
||||||
|
return wp.repeat
|
||||||
|
}
|
111
pkg/wallpaper/wallpaper_test.go
Normal file
111
pkg/wallpaper/wallpaper_test.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWallpaper(t *testing.T) {
|
||||||
|
var testFunc = func(width, height int) {
|
||||||
|
var (
|
||||||
|
qWidth = width / 2
|
||||||
|
qHeight = height / 2
|
||||||
|
red = color.RGBA{255, 0, 0, 255}
|
||||||
|
green = color.RGBA{0, 255, 0, 255}
|
||||||
|
blue = color.RGBA{0, 0, 255, 255}
|
||||||
|
pink = color.RGBA{255, 0, 255, 255}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a dummy image that is width*height and has the four
|
||||||
|
// quadrants laid out as solid colors:
|
||||||
|
// Red | Green
|
||||||
|
// Blue | Pink
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
draw.Draw(
|
||||||
|
// Corner: red
|
||||||
|
img, // dst Image
|
||||||
|
image.Rect(0, 0, qWidth, qHeight), // r Rectangle
|
||||||
|
image.NewUniform(red), // src Image
|
||||||
|
image.Point{0, 0}, // sp Point
|
||||||
|
draw.Over, // op Op
|
||||||
|
)
|
||||||
|
draw.Draw(
|
||||||
|
// Top: green
|
||||||
|
img,
|
||||||
|
image.Rect(qWidth, 0, width, qHeight),
|
||||||
|
image.NewUniform(green),
|
||||||
|
image.Point{qWidth, 0},
|
||||||
|
draw.Over,
|
||||||
|
)
|
||||||
|
draw.Draw(
|
||||||
|
// Left: blue
|
||||||
|
img,
|
||||||
|
image.Rect(0, qHeight, qWidth, height),
|
||||||
|
image.NewUniform(blue),
|
||||||
|
image.Point{0, qHeight},
|
||||||
|
draw.Over,
|
||||||
|
)
|
||||||
|
draw.Draw(
|
||||||
|
// Repeat: pink
|
||||||
|
img,
|
||||||
|
image.Rect(qWidth, qHeight, width, height),
|
||||||
|
image.NewUniform(pink),
|
||||||
|
image.Point{qWidth, qHeight},
|
||||||
|
draw.Over,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Output as png to disk if you wanna see what's in it.
|
||||||
|
if os.Getenv("T_WALLPAPER_PNG") != "" {
|
||||||
|
fn := fmt.Sprintf("test-%dx%d.png", width, height)
|
||||||
|
if fh, err := os.Create(fn); err == nil {
|
||||||
|
defer fh.Close()
|
||||||
|
if err := png.Encode(fh, img); err != nil {
|
||||||
|
t.Errorf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp, err := FromImage(nil, img, "dummy")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Couldn't create FromImage: %s", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the quarter size is what we expected.
|
||||||
|
w, h := wp.QuarterSize()
|
||||||
|
if w != qWidth || h != qHeight {
|
||||||
|
t.Errorf(
|
||||||
|
"Got wrong quarter size: expected %dx%d but got %dx%d",
|
||||||
|
qWidth, qHeight,
|
||||||
|
w, h,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the colors.
|
||||||
|
testColor := func(name string, img *image.RGBA, expect color.RGBA) {
|
||||||
|
if actual := img.At(5, 5); actual != expect {
|
||||||
|
t.Errorf(
|
||||||
|
"%s: expected color %v but got %v",
|
||||||
|
name,
|
||||||
|
expect,
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testColor("Corner", wp.Corner(), red)
|
||||||
|
testColor("Top", wp.Top(), green)
|
||||||
|
testColor("Left", wp.Left(), blue)
|
||||||
|
testColor("Repeat", wp.Repeat(), pink)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFunc(128, 128)
|
||||||
|
testFunc(128, 64)
|
||||||
|
testFunc(64, 128)
|
||||||
|
testFunc(12, 12)
|
||||||
|
testFunc(57, 39)
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ type PlayScene struct {
|
||||||
Level *level.Level
|
Level *level.Level
|
||||||
|
|
||||||
// Private variables.
|
// Private variables.
|
||||||
|
d *Doodle
|
||||||
drawing *uix.Canvas
|
drawing *uix.Canvas
|
||||||
|
|
||||||
// Player character
|
// Player character
|
||||||
|
@ -32,6 +33,7 @@ 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.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)))
|
||||||
|
@ -40,7 +42,7 @@ 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(s.Level)
|
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
||||||
s.drawing.InstallActors(s.Level.Actors)
|
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)
|
||||||
|
@ -53,7 +55,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
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(s.Level)
|
s.drawing.LoadLevel(d.Engine, s.Level)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Flash("Entered Play Mode. Press 'E' to edit this map.")
|
d.Flash("Entered Play Mode. Press 'E' to edit this map.")
|
||||||
|
@ -161,7 +163,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Level = level
|
s.Level = level
|
||||||
s.drawing.LoadLevel(s.Level)
|
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
||||||
s.drawing.InstallActors(s.Level.Actors)
|
s.drawing.InstallActors(s.Level.Actors)
|
||||||
s.drawing.AddActor(s.Player)
|
s.drawing.AddActor(s.Player)
|
||||||
|
|
||||||
|
|
69
render/functions.go
Normal file
69
render/functions.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
// TrimBox helps with Engine.Copy() to trim a destination box so that it
|
||||||
|
// won't overflow with the parent container.
|
||||||
|
func TrimBox(src, dst *Rect, p Point, S Rect, thickness int32) {
|
||||||
|
// Constrain source width to not bigger than Canvas width.
|
||||||
|
if src.W > S.W {
|
||||||
|
src.W = S.W
|
||||||
|
}
|
||||||
|
if src.H > S.H {
|
||||||
|
src.H = S.H
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 - thickness) - (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 - thickness) - (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 + thickness
|
||||||
|
dst.W -= delta
|
||||||
|
src.X += delta
|
||||||
|
}
|
||||||
|
if dst.Y < p.Y {
|
||||||
|
delta := p.Y - dst.Y
|
||||||
|
dst.Y = p.Y + thickness
|
||||||
|
dst.H -= delta
|
||||||
|
src.Y += delta
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the destination width so it doesn't overlap the Canvas border.
|
||||||
|
if dst.W >= S.W-thickness {
|
||||||
|
dst.W = S.W - thickness
|
||||||
|
}
|
||||||
|
}
|
80
shell.go
80
shell.go
|
@ -206,51 +206,51 @@ func (s *Shell) Parse(input string) Command {
|
||||||
|
|
||||||
// Draw the shell.
|
// Draw the shell.
|
||||||
func (s *Shell) Draw(d *Doodle, ev *events.State) error {
|
func (s *Shell) Draw(d *Doodle, ev *events.State) error {
|
||||||
if ev.EscapeKey.Read() {
|
|
||||||
s.Close()
|
|
||||||
return nil
|
|
||||||
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
|
|
||||||
s.Execute(s.Text)
|
|
||||||
|
|
||||||
// Auto-close the console unless in REPL mode.
|
|
||||||
if !s.Repl {
|
|
||||||
s.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
|
|
||||||
// Paging through history.
|
|
||||||
if !s.historyPaging {
|
|
||||||
s.historyPaging = true
|
|
||||||
s.historyIndex = len(s.History)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume the inputs and make convenient variables.
|
|
||||||
ev.Down.Read()
|
|
||||||
isUp := ev.Up.Read()
|
|
||||||
|
|
||||||
// Scroll through the input history.
|
|
||||||
if isUp {
|
|
||||||
s.historyIndex--
|
|
||||||
if s.historyIndex < 0 {
|
|
||||||
s.historyIndex = 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.historyIndex++
|
|
||||||
if s.historyIndex >= len(s.History) {
|
|
||||||
s.historyIndex = len(s.History) - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Text = s.History[s.historyIndex]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the line height we can draw.
|
// Compute the line height we can draw.
|
||||||
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
|
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
|
||||||
|
|
||||||
// If the console is open, draw the console.
|
// If the console is open, draw the console.
|
||||||
if s.Open {
|
if s.Open {
|
||||||
|
if ev.EscapeKey.Read() {
|
||||||
|
s.Close()
|
||||||
|
return nil
|
||||||
|
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
|
||||||
|
s.Execute(s.Text)
|
||||||
|
|
||||||
|
// Auto-close the console unless in REPL mode.
|
||||||
|
if !s.Repl {
|
||||||
|
s.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
|
||||||
|
// Paging through history.
|
||||||
|
if !s.historyPaging {
|
||||||
|
s.historyPaging = true
|
||||||
|
s.historyIndex = len(s.History)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the inputs and make convenient variables.
|
||||||
|
ev.Down.Read()
|
||||||
|
isUp := ev.Up.Read()
|
||||||
|
|
||||||
|
// Scroll through the input history.
|
||||||
|
if isUp {
|
||||||
|
s.historyIndex--
|
||||||
|
if s.historyIndex < 0 {
|
||||||
|
s.historyIndex = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.historyIndex++
|
||||||
|
if s.historyIndex >= len(s.History) {
|
||||||
|
s.historyIndex = len(s.History) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Text = s.History[s.historyIndex]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Cursor flip?
|
// Cursor flip?
|
||||||
if d.ticks > s.cursorFlip {
|
if d.ticks > s.cursorFlip {
|
||||||
s.cursorFlip = d.ticks + s.cursorRate
|
s.cursorFlip = d.ticks + s.cursorRate
|
||||||
|
|
|
@ -2,6 +2,7 @@ package uix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"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/pkg/wallpaper"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/ui"
|
||||||
)
|
)
|
||||||
|
@ -32,6 +34,10 @@ type Canvas struct {
|
||||||
// to remove the mask.
|
// to remove the mask.
|
||||||
MaskColor render.Color
|
MaskColor render.Color
|
||||||
|
|
||||||
|
// Debug tools
|
||||||
|
// NoLimitScroll suppresses the scroll limit for bounded levels.
|
||||||
|
NoLimitScroll bool
|
||||||
|
|
||||||
// Underlying chunk data for the drawing.
|
// Underlying chunk data for the drawing.
|
||||||
chunks *level.Chunker
|
chunks *level.Chunker
|
||||||
|
|
||||||
|
@ -39,6 +45,9 @@ type Canvas struct {
|
||||||
actor *level.Actor // if this canvas IS an actor
|
actor *level.Actor // if this canvas IS an actor
|
||||||
actors []*Actor
|
actors []*Actor
|
||||||
|
|
||||||
|
// Wallpaper settings.
|
||||||
|
wallpaper *Wallpaper
|
||||||
|
|
||||||
// When the Canvas wants to delete Actors, but ultimately it is upstream
|
// When the Canvas wants to delete Actors, but ultimately it is upstream
|
||||||
// that controls the actors. Upstream should delete them and then reinstall
|
// that controls the actors. Upstream should delete them and then reinstall
|
||||||
// the actor list from scratch.
|
// the actor list from scratch.
|
||||||
|
@ -64,6 +73,7 @@ func NewCanvas(size int, editable bool) *Canvas {
|
||||||
Palette: level.NewPalette(),
|
Palette: level.NewPalette(),
|
||||||
chunks: level.NewChunker(size),
|
chunks: level.NewChunker(size),
|
||||||
actors: make([]*Actor, 0),
|
actors: make([]*Actor, 0),
|
||||||
|
wallpaper: &Wallpaper{},
|
||||||
}
|
}
|
||||||
w.setup()
|
w.setup()
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
|
@ -95,8 +105,27 @@ func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadLevel initializes a Canvas from a Level object.
|
// LoadLevel initializes a Canvas from a Level object.
|
||||||
func (w *Canvas) LoadLevel(level *level.Level) {
|
func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) {
|
||||||
w.Load(level.Palette, level.Chunker)
|
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.
|
// LoadDoodad initializes a Canvas from a Doodad object.
|
||||||
|
@ -262,6 +291,44 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
H: S.H - 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.
|
// Get the chunks in the viewport and cache their textures.
|
||||||
for coord := range w.chunks.IterViewportChunks(Viewport) {
|
for coord := range w.chunks.IterViewportChunks(Viewport) {
|
||||||
if chunk, ok := w.chunks.GetChunk(coord); ok {
|
if chunk, ok := w.chunks.GetChunk(coord); ok {
|
||||||
|
@ -298,6 +365,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
H: src.H,
|
H: src.H,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: all this shit is in TrimBox(), make it DRY
|
||||||
|
|
||||||
// If the destination width will cause it to overflow the widget
|
// If the destination width will cause it to overflow the widget
|
||||||
// box, trim off the right edge of the destination rect.
|
// box, trim off the right edge of the destination rect.
|
||||||
//
|
//
|
||||||
|
|
175
uix/canvas_wallpaper.go
Normal file
175
uix/canvas_wallpaper.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package uix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/wallpaper"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wallpaper configures the wallpaper in a Canvas.
|
||||||
|
type Wallpaper struct {
|
||||||
|
pageType level.PageType
|
||||||
|
maxWidth int64
|
||||||
|
maxHeight int64
|
||||||
|
corner render.Texturer
|
||||||
|
top render.Texturer
|
||||||
|
left render.Texturer
|
||||||
|
repeat render.Texturer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid returns whether the Wallpaper is configured. Only Levels should
|
||||||
|
// have wallpapers and Doodads will have nil ones.
|
||||||
|
func (wp *Wallpaper) Valid() bool {
|
||||||
|
return wp.repeat != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresentWallpaper draws the wallpaper.
|
||||||
|
func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
|
||||||
|
var (
|
||||||
|
wp = w.wallpaper
|
||||||
|
S = w.Size()
|
||||||
|
size = wp.corner.Size()
|
||||||
|
Viewport = w.ViewportRelative()
|
||||||
|
origin = render.Point{
|
||||||
|
X: p.X + w.Scroll.X + w.BoxThickness(1),
|
||||||
|
Y: p.Y + w.Scroll.Y + w.BoxThickness(1),
|
||||||
|
}
|
||||||
|
limit = render.Point{
|
||||||
|
// NOTE: we add + the texture size so we would actually draw one
|
||||||
|
// full extra texture out-of-bounds for the repeating backgrounds.
|
||||||
|
// This is cuz for scrolling we offset the draw spot on a loop.
|
||||||
|
X: origin.X + S.W - w.BoxThickness(1) + size.W,
|
||||||
|
Y: origin.Y + S.H - w.BoxThickness(1) + size.H,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// For tiled textures, compute the offset amount. If we are scrolled away
|
||||||
|
// from the Origin (0,0) we find out by how far (subtract full tile sizes)
|
||||||
|
// and use the remainder as an offset for drawing the tiles.
|
||||||
|
var dx, dy int32
|
||||||
|
if origin.X > 0 {
|
||||||
|
for origin.X > 0 && origin.X > size.W {
|
||||||
|
origin.X -= size.W
|
||||||
|
}
|
||||||
|
dx = origin.X
|
||||||
|
origin.X = 0
|
||||||
|
}
|
||||||
|
if origin.Y > 0 {
|
||||||
|
for origin.Y > 0 && origin.Y > size.H {
|
||||||
|
origin.Y -= size.H
|
||||||
|
}
|
||||||
|
dy = origin.Y
|
||||||
|
origin.Y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// And capping the scroll delta in the other direction.
|
||||||
|
if limit.X < S.W {
|
||||||
|
limit.X = S.W
|
||||||
|
}
|
||||||
|
if limit.Y < S.H {
|
||||||
|
// TODO: slight flicker on bottom edge when scrolling down
|
||||||
|
limit.Y = S.H
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile the repeat texture.
|
||||||
|
for x := origin.X - size.W; x < limit.X; x += size.W {
|
||||||
|
for y := origin.Y - size.H; y < limit.Y; y += size.H {
|
||||||
|
src := render.Rect{
|
||||||
|
W: size.W,
|
||||||
|
H: size.H,
|
||||||
|
}
|
||||||
|
dst := render.Rect{
|
||||||
|
X: x + dx,
|
||||||
|
Y: y + dy,
|
||||||
|
W: src.W,
|
||||||
|
H: src.H,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the edges of the destination box, like in canvas.go#Present
|
||||||
|
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
|
||||||
|
|
||||||
|
e.Copy(wp.repeat, src, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The left edge corner tiled along the left edge.
|
||||||
|
if wp.pageType > level.Unbounded {
|
||||||
|
for y := origin.Y; y < limit.Y; y += size.H {
|
||||||
|
src := render.Rect{
|
||||||
|
W: size.W,
|
||||||
|
H: size.H,
|
||||||
|
}
|
||||||
|
dst := render.Rect{
|
||||||
|
X: origin.X,
|
||||||
|
Y: y + dy,
|
||||||
|
W: src.W,
|
||||||
|
H: src.H,
|
||||||
|
}
|
||||||
|
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
|
||||||
|
e.Copy(wp.left, src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The top edge tiled along the top edge.
|
||||||
|
for x := origin.X; x < limit.X; x += size.W {
|
||||||
|
src := render.Rect{
|
||||||
|
W: size.W,
|
||||||
|
H: size.H,
|
||||||
|
}
|
||||||
|
dst := render.Rect{
|
||||||
|
X: x,
|
||||||
|
Y: origin.Y,
|
||||||
|
W: src.W,
|
||||||
|
H: src.H,
|
||||||
|
}
|
||||||
|
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
|
||||||
|
e.Copy(wp.top, src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The top left corner for all page types except Unbounded.
|
||||||
|
if Viewport.Intersects(size) {
|
||||||
|
src := render.Rect{
|
||||||
|
W: size.W,
|
||||||
|
H: size.H,
|
||||||
|
}
|
||||||
|
dst := render.Rect{
|
||||||
|
X: origin.X,
|
||||||
|
Y: origin.Y,
|
||||||
|
W: src.W,
|
||||||
|
H: src.H,
|
||||||
|
}
|
||||||
|
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
|
||||||
|
e.Copy(wp.corner, src, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the wallpaper settings from a level.
|
||||||
|
func (wp *Wallpaper) Load(e render.Engine, pageType level.PageType, v *wallpaper.Wallpaper) error {
|
||||||
|
wp.pageType = pageType
|
||||||
|
if tex, err := v.CornerTexture(e); err == nil {
|
||||||
|
wp.corner = tex
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tex, err := v.TopTexture(e); err == nil {
|
||||||
|
wp.top = tex
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tex, err := v.LeftTexture(e); err == nil {
|
||||||
|
wp.left = tex
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tex, err := v.RepeatTexture(e); err == nil {
|
||||||
|
wp.repeat = tex
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user