diff --git a/Makefile b/Makefile index 7f2371e..2f8e76a 100644 --- a/Makefile +++ b/Makefile @@ -42,12 +42,12 @@ build-debug: # `make bindata` generates the embedded binary assets package. .PHONY: bindata bindata: - go-bindata -pkg bindata -o pkg/bindata/bindata.go assets/... fonts/ + go-bindata -pkg bindata -o pkg/bindata/bindata.go assets/... # `make bindata-dev` generates the debug version of bindata package. .PHONY: bindata-dev bindata-dev: - go-bindata -debug -pkg bindata -o pkg/bindata/bindata.go assets/... fonts/ + go-bindata -debug -pkg bindata -o pkg/bindata/bindata.go assets/... # `make wasm` builds the WebAssembly port. .PHONY: wasm @@ -102,7 +102,7 @@ test: dist: doodads bindata build mkdir -p dist/doodle-$(VERSION) cp bin/* dist/doodle-$(VERSION)/ - cp -r assets fonts README.md dist/doodle-$(VERSION)/ + cp -r README.md dist/doodle-$(VERSION)/ cd dist && tar -czvf doodle-$(VERSION).tar.gz doodle-$(VERSION) cd dist && zip -r doodle-$(VERSION).zip doodle-$(VERSION) diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 029ed4f..5858185 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -11,6 +11,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render/sdl" doodle "git.kirsle.net/apps/doodle/pkg" "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" "github.com/urfave/cli" @@ -80,6 +81,20 @@ func main() { balance.Height, ) + // Load the SDL fonts in from bindata storage. + if fonts, err := bindata.AssetDir("assets/fonts"); err == nil { + for _, file := range fonts { + data, err := bindata.Asset("assets/fonts/" + file) + if err != nil { + panic(err) + } + + sdl.InstallFont(file, data) + } + } else { + panic(err) + } + game := doodle.New(c.Bool("debug"), engine) game.SetupEngine() if c.Bool("guitest") { diff --git a/lib/render/sdl/events.go b/lib/render/sdl/events.go index 7c90a1a..6c958da 100644 --- a/lib/render/sdl/events.go +++ b/lib/render/sdl/events.go @@ -13,7 +13,7 @@ var ( DebugWindowEvents = false DebugMouseEvents = false DebugClickEvents = false - DebugKeyEvents = true + DebugKeyEvents = false ) // Poll for events. diff --git a/lib/render/sdl/text.go b/lib/render/sdl/text.go index caac55d..efe59fd 100644 --- a/lib/render/sdl/text.go +++ b/lib/render/sdl/text.go @@ -3,6 +3,7 @@ package sdl import ( "fmt" "strings" + "sync" "git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/render" @@ -11,9 +12,29 @@ import ( ) // TODO: font filenames -var defaultFontFilename = "./fonts/DejaVuSans.ttf" +var defaultFontFilename = "DejaVuSans.ttf" -var fonts = map[string]*ttf.Font{} +// Font holds cached SDL_TTF structures for loaded fonts. They are created +// automatically when fonts are either preinstalled (InstallFont) or loaded for +// the first time as demanded by the DrawText method. +type Font struct { + Filename string + data []byte // raw binary data of font + ttf *ttf.Font +} + +var ( + fonts = map[string]*ttf.Font{} // keys like "DejaVuSans@14" by font size + installedFont = map[string][]byte{} // installed font files' binary handles + fontsMu sync.RWMutex +) + +// InstallFont preloads the font cache using TTF binary data in memory. +func InstallFont(filename string, binary []byte) { + fontsMu.Lock() + installedFont[filename] = binary + fontsMu.Unlock() +} // LoadFont loads and caches the font at a given size. func LoadFont(filename string, size int) (*ttf.Font, error) { @@ -27,10 +48,30 @@ func LoadFont(filename string, size int) (*ttf.Font, error) { return font, nil } - font, err := ttf.OpenFont(filename, size) - if err != nil { - return nil, err + // Do we have this font in memory? + var ( + font *ttf.Font + err error + ) + + if binary, ok := installedFont[filename]; ok { + var RWops *sdl.RWops + RWops, err = sdl.RWFromMem(binary) + if err != nil { + return nil, fmt.Errorf("LoadFont(%s): RWFromMem: %s", filename, err) + } + + font, err = ttf.OpenFontRW(RWops, 0, size) + } else { + font, err = ttf.OpenFont(filename, size) } + + // Error opening the font? + if err != nil { + return nil, fmt.Errorf("LoadFont(%s): %s", filename, err) + } + + // Cache this font name and size. fonts[keyName] = font return font, nil diff --git a/pkg/balance/debug.go b/pkg/balance/debug.go index 5e51ba8..4365efb 100644 --- a/pkg/balance/debug.go +++ b/pkg/balance/debug.go @@ -16,7 +16,7 @@ var ( ***************/ // Debug overlay (FPS etc.) settings. - DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf" + DebugFontFilename = "DejaVuSans-Bold.ttf" DebugFontSize = 16 DebugLabelColor = render.MustHexColor("#FF9900") DebugValueColor = render.MustHexColor("#00CCFF") diff --git a/pkg/balance/shell.go b/pkg/balance/shell.go index f0926b9..a5b361b 100644 --- a/pkg/balance/shell.go +++ b/pkg/balance/shell.go @@ -4,7 +4,7 @@ import "git.kirsle.net/apps/doodle/lib/render" // Shell related variables. var ( - ShellFontFilename = "./fonts/DejaVuSansMono.ttf" + ShellFontFilename = "DejaVuSansMono.ttf" ShellBackgroundColor = render.RGBA(0, 20, 40, 200) ShellForegroundColor = render.RGBA(0, 153, 255, 255) ShellPromptColor = render.White diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index c7c26f0..15f3c89 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -14,7 +14,7 @@ var ( OutlineColor: render.Black, } TitleFont = render.Text{ - FontFilename: "./fonts/DejaVuSans-Bold.ttf", + FontFilename: "DejaVuSans-Bold.ttf", Size: 12, Padding: 4, Color: render.White, @@ -47,7 +47,7 @@ var ( // LabelFont is the font for strong labels in UI. LabelFont = render.Text{ Size: 12, - FontFilename: "./fonts/DejaVuSans-Bold.ttf", + FontFilename: "DejaVuSans-Bold.ttf", Padding: 4, Color: render.Black, } @@ -56,7 +56,7 @@ var ( DragColor = render.MustHexColor("#0099FF") PlayButtonFont = render.Text{ - FontFilename: "./fonts/DejaVuSans-Bold.ttf", + FontFilename: "DejaVuSans-Bold.ttf", Size: 16, Padding: 4, Color: render.RGBA(255, 255, 0, 255), diff --git a/pkg/doodads/fmt_readwrite.go b/pkg/doodads/fmt_readwrite.go index fcc2fd3..1666350 100644 --- a/pkg/doodads/fmt_readwrite.go +++ b/pkg/doodads/fmt_readwrite.go @@ -46,7 +46,18 @@ func ListDoodads() ([]string, error) { // Append user doodads. userFiles, err := userdir.ListDoodads() names = append(names, userFiles...) - return names, err + + // Deduplicate names. + var uniq = map[string]interface{}{} + var result []string + for _, name := range names { + if _, ok := uniq[name]; !ok { + uniq[name] = nil + result = append(result, name) + } + } + + return result, err } // LoadFile reads a doodad file from disk, checking a few locations. diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 3dc5b9e..d36bf70 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -302,14 +302,16 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { } // A link event to connect two actors together. - drawing.OnLinkActors = func(a, b *level.Actor) { - d.Flash("Link %s and %s", a.Filename, b.Filename) - idA, idB := a.ID(), b.ID() - a.AddLink(idB) - b.AddLink(idA) + drawing.OnLinkActors = func(a, b *uix.Actor) { + // The actors are a uix.Actor which houses a level.Actor which we + // want to update to map each other's IDs. + idA, idB := a.Actor.ID(), b.Actor.ID() + a.Actor.AddLink(idB) + b.Actor.AddLink(idA) // Reset the Link tool. drawing.Tool = uix.ActorTool + d.Flash("Linked '%s' and '%s' together", a.Doodad.Title, b.Doodad.Title) } // Set up the drop handler for draggable doodads. diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 2eb83ff..84465f3 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -6,8 +6,10 @@ import ( "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/branding" + "git.kirsle.net/apps/doodle/pkg/filesystem" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/uix" ) @@ -17,7 +19,8 @@ type MainScene struct { frame *ui.Frame // Background wallpaper canvas. - canvas *uix.Canvas + scripting *scripting.Supervisor + canvas *uix.Canvas } // Name of the scene. @@ -29,18 +32,9 @@ func (s *MainScene) Name() string { func (s *MainScene) Setup(d *Doodle) error { s.Supervisor = ui.NewSupervisor() - // Set up the background wallpaper canvas. - s.canvas = uix.NewCanvas(100, false) - s.canvas.Resize(render.Rect{ - W: int32(d.width), - H: int32(d.height), - }) - s.canvas.LoadLevel(d.Engine, &level.Level{ - Chunker: level.NewChunker(100), - Palette: level.NewPalette(), - PageType: level.Bounded, - Wallpaper: "notebook.png", - }) + if err := s.SetupDemoLevel(d); err != nil { + return err + } // Main UI button frame. frame := ui.NewFrame("frame") @@ -78,10 +72,52 @@ func (s *MainScene) Setup(d *Doodle) error { return nil } +// SetupDemoLevel configures the wallpaper behind the New screen, +// which demos a title screen demo level. +func (s *MainScene) SetupDemoLevel(d *Doodle) error { + // Set up the background wallpaper canvas. + s.canvas = uix.NewCanvas(100, false) + s.canvas.Scrollable = true + s.canvas.Resize(render.Rect{ + W: int32(d.width), + H: int32(d.height), + }) + + // Title screen level to load. + lvlName, _ := filesystem.FindFile("example1.level") + lvl, err := level.LoadJSON(lvlName) + if err != nil { + log.Error("Error loading title-screen.level: %s", err) + } + + s.canvas.LoadLevel(d.Engine, lvl) + s.canvas.InstallActors(lvl.Actors) + + // Load all actor scripts. + s.scripting = scripting.NewSupervisor() + s.canvas.SetScriptSupervisor(s.scripting) + if err := s.scripting.InstallScripts(lvl); err != nil { + log.Error("Error with title screen level scripts: %s", err) + } + + // Run all actors scripts main function to start them off. + if err := s.canvas.InstallScripts(); err != nil { + log.Error("Error running actor main() functions: %s", err) + } + + return nil +} + // Loop the editor scene. func (s *MainScene) Loop(d *Doodle, ev *events.State) error { s.Supervisor.Loop(ev) + if err := s.scripting.Loop(); err != nil { + log.Error("MainScene.Loop: scripting.Loop: %s", err) + } + + s.canvas.Loop(ev) + if resized := ev.Resized.Read(); resized { w, h := d.Engine.WindowSize() d.width = w @@ -103,6 +139,14 @@ func (s *MainScene) Draw(d *Doodle) error { s.canvas.Present(d.Engine, render.Origin) + // Draw a sheen over the level for clarity. + d.Engine.DrawBox(render.RGBA(255, 255, 254, 128), render.Rect{ + X: 0, + Y: 0, + W: int32(d.width), + H: int32(d.height), + }) + label := ui.NewLabel(ui.Label{ Text: branding.AppName, Font: render.Text{ diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index ccc5191..4d92ead 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -10,6 +10,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" @@ -69,7 +70,7 @@ type Canvas struct { // -- WHEN Canvas.Tool is "Link" -- // When the Canvas wants to link two actors together. Arguments are the IDs // of the two actors. - OnLinkActors func(a, b *level.Actor) + OnLinkActors func(a, b *Actor) linkFirst *Actor // Tracking pixels while editing. TODO: get rid of pixelHistory? @@ -129,9 +130,12 @@ func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) { // TODO: wallpaper paths filename := "assets/wallpapers/" + level.Wallpaper if runtime.GOOS != "js" { - if _, err := os.Stat(filename); os.IsNotExist(err) { - log.Error("LoadLevel: %s", err) - filename = "assets/wallpapers/notebook.png" // XXX TODO + // Check if the wallpaper wasn't found. Check bindata and file system. + if _, err := bindata.Asset(filename); err != nil { + if _, err := os.Stat(filename); os.IsNotExist(err) { + log.Error("LoadLevel: wallpaper %s did not appear to exist, default to notebook.png", filename) + filename = "assets/wallpapers/notebook.png" + } } } diff --git a/pkg/uix/canvas_link_tool.go b/pkg/uix/canvas_link_tool.go index e29b074..27e51b7 100644 --- a/pkg/uix/canvas_link_tool.go +++ b/pkg/uix/canvas_link_tool.go @@ -1,6 +1,10 @@ package uix -import "errors" +import ( + "errors" + + "git.kirsle.net/apps/doodle/pkg/shmem" +) // LinkStart initializes the Link tool. func (w *Canvas) LinkStart() { @@ -13,10 +17,13 @@ func (w *Canvas) LinkAdd(a *Actor) error { if w.linkFirst == nil { // First click, hold onto this actor. w.linkFirst = a + shmem.Flash("Doodad '%s' selected, click the next Doodad to link it to", + a.Doodad.Title, + ) } else { // Second click, call the OnLinkActors handler with the two actors. if w.OnLinkActors != nil { - w.OnLinkActors(w.linkFirst.Actor, a.Actor) + w.OnLinkActors(w.linkFirst, a) } else { return errors.New("Canvas.LinkAdd: no OnLinkActors handler is ready") } diff --git a/pkg/wallpaper/wallpaper.go b/pkg/wallpaper/wallpaper.go index 5dacad7..7c9a56f 100644 --- a/pkg/wallpaper/wallpaper.go +++ b/pkg/wallpaper/wallpaper.go @@ -1,6 +1,7 @@ package wallpaper import ( + "bytes" "image" "image/draw" "os" @@ -9,6 +10,7 @@ import ( "strings" "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/bindata" ) // Wallpaper is a repeatable background image to go behind levels. @@ -64,14 +66,28 @@ func FromFile(e render.Engine, filename string) (*Wallpaper, error) { }, nil } - fh, err := os.Open(filename) - if err != nil { - return nil, err + // Try and get an image object by any means. + var ( + img image.Image + format string + imgErr error + ) + + // Try the bindata store. + if data, err := bindata.Asset(filename); err == nil { + fh := bytes.NewBuffer(data) + img, format, imgErr = image.Decode(fh) + } else { + fh, err := os.Open(filename) + if err != nil { + return nil, err + } + img, format, imgErr = image.Decode(fh) } - img, format, err := image.Decode(fh) - if err != nil { - return nil, err + // Image loading error? + if imgErr != nil { + return nil, imgErr } // Ugly hack: make it an image.RGBA because the thing we get tends to be diff --git a/wasm/server.go b/wasm/server.go index 48c64db..919b57a 100644 --- a/wasm/server.go +++ b/wasm/server.go @@ -20,7 +20,6 @@ func main() { go watchChanges() http.Handle("/", http.FileServer(http.Dir("."))) - http.Handle("/fonts", http.FileServer(http.Dir("../fonts/"))) http.HandleFunc(wasm, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/wasm") http.ServeFile(w, r, "."+wasm)