Colliding Doodads #1
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ maps/
|
|||
bin/
|
||||
screenshot-*.png
|
||||
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
|
||||
in the Editor Mode, in pixels per tick.
|
||||
* `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/balance"
|
||||
"git.kirsle.net/apps/doodle/render/sdl"
|
||||
|
||||
_ "image/png"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
UI *EditorUI
|
||||
d *Doodle
|
||||
|
||||
// The current level or doodad object being edited, based on the
|
||||
// DrawingType.
|
||||
|
@ -43,6 +44,7 @@ func (s *EditorScene) Name() string {
|
|||
func (s *EditorScene) Setup(d *Doodle) error {
|
||||
// Initialize the user interface. It references the palette and such so it
|
||||
// must be initialized after those things.
|
||||
s.d = d
|
||||
s.UI = NewEditorUI(d, s)
|
||||
|
||||
// Were we given configuration data?
|
||||
|
@ -57,7 +59,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
case enum.LevelDrawing:
|
||||
if s.Level != nil {
|
||||
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)
|
||||
} else if s.filename != "" && s.OpenFile {
|
||||
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")
|
||||
s.Level = level.New()
|
||||
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.Scrollable = true
|
||||
}
|
||||
|
@ -154,7 +156,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
|||
|
||||
s.DrawingType = enum.LevelDrawing
|
||||
s.Level = level
|
||||
s.UI.Canvas.LoadLevel(s.Level)
|
||||
s.UI.Canvas.LoadLevel(s.d.Engine, s.Level)
|
||||
|
||||
// TODO: debug
|
||||
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)
|
||||
}
|
||||
|
||||
// Fill in defaults.
|
||||
if m.Wallpaper == "" {
|
||||
m.Wallpaper = DefaultWallpaper
|
||||
}
|
||||
|
||||
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
||||
m.Chunker.Inflate(m.Palette)
|
||||
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"
|
||||
)
|
||||
|
||||
// Useful variables.
|
||||
var (
|
||||
DefaultWallpaper = "notebook.png"
|
||||
)
|
||||
|
||||
// Base provides the common struct keys that are shared between Levels and
|
||||
// Doodads.
|
||||
type Base struct {
|
||||
|
@ -33,6 +38,12 @@ type Level struct {
|
|||
// properties (solid, fire, slippery, etc.)
|
||||
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 ActorMap `json:"actors"`
|
||||
}
|
||||
|
@ -46,6 +57,11 @@ func New() *Level {
|
|||
Chunker: NewChunker(balance.ChunkSize),
|
||||
Palette: &Palette{},
|
||||
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
|
||||
|
||||
// Private variables.
|
||||
d *Doodle
|
||||
drawing *uix.Canvas
|
||||
|
||||
// Player character
|
||||
|
@ -32,6 +33,7 @@ func (s *PlayScene) Name() string {
|
|||
|
||||
// Setup the play scene.
|
||||
func (s *PlayScene) Setup(d *Doodle) error {
|
||||
s.d = d
|
||||
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
||||
s.drawing.MoveTo(render.Origin)
|
||||
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?
|
||||
if s.Level != nil {
|
||||
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)
|
||||
} else if 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 {
|
||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
||||
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.")
|
||||
|
@ -161,7 +163,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
|||
}
|
||||
|
||||
s.Level = level
|
||||
s.drawing.LoadLevel(s.Level)
|
||||
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
||||
s.drawing.InstallActors(s.Level.Actors)
|
||||
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.
|
||||
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.
|
||||
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
|
||||
|
||||
// If the console is open, draw the console.
|
||||
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?
|
||||
if d.ticks > s.cursorFlip {
|
||||
s.cursorFlip = d.ticks + s.cursorRate
|
||||
|
|
|
@ -2,6 +2,7 @@ package uix
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/balance"
|
||||
|
@ -9,6 +10,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
@ -32,6 +34,10 @@ type Canvas struct {
|
|||
// 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
|
||||
|
||||
|
@ -39,6 +45,9 @@ type Canvas struct {
|
|||
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.
|
||||
|
@ -64,6 +73,7 @@ func NewCanvas(size int, editable bool) *Canvas {
|
|||
Palette: level.NewPalette(),
|
||||
chunks: level.NewChunker(size),
|
||||
actors: make([]*Actor, 0),
|
||||
wallpaper: &Wallpaper{},
|
||||
}
|
||||
w.setup()
|
||||
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.
|
||||
func (w *Canvas) LoadLevel(level *level.Level) {
|
||||
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.
|
||||
|
@ -262,6 +291,44 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
|||
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 {
|
||||
|
@ -298,6 +365,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
|||
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.
|
||||
//
|
||||
|
|
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