diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 2cf541f..4e2e678 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -14,15 +14,15 @@ var ( ScrollboxHoz = 256 // horizontal px from window border to start scrol ScrollboxVert = 160 // NEW: set scrollbox bounds by percents - ScrollboxHozPercent float64 = 0.25 + ScrollboxHozPercent float64 = 0.25 ScrollboxVertPercent float64 = 0.40 // Player speeds - PlayerMaxVelocity float64 = 6 - PlayerAcceleration float64 = 0.9 - Gravity float64 = 6 + PlayerMaxVelocity float64 = 6 + PlayerAcceleration float64 = 0.9 + Gravity float64 = 6 GravityAcceleration float64 = 0.2 - SlopeMaxHeight = 8 // max pixel height for player to walk up a slope + SlopeMaxHeight = 8 // max pixel height for player to walk up a slope // Default chunk size for canvases. ChunkSize = 128 @@ -57,8 +57,12 @@ var ( // Level attachment filename for the custom wallpaper. // NOTE: due to hard-coded "assets/wallpapers/" prefix in uix/canvas.go#LoadLevel. - CustomWallpaperFilename = "custom.b64img" + CustomWallpaperFilename = "custom.b64img" CustomWallpaperEmbedPath = "assets/wallpapers/custom.b64img" + + // Publishing: Doodads-embedded-within-levels. + EmbeddedDoodadsBasePath = "assets/doodads/" + EmbeddedWallpaperBasePath = "assets/wallpapers/" ) // Edit Mode Values diff --git a/pkg/doodads/fmt_readwrite.go b/pkg/doodads/fmt_readwrite.go index 31b9d99..ded12ee 100644 --- a/pkg/doodads/fmt_readwrite.go +++ b/pkg/doodads/fmt_readwrite.go @@ -1,12 +1,14 @@ package doodads import ( + "errors" "fmt" "io/ioutil" "runtime" "sort" "strings" + "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/enum" @@ -16,6 +18,11 @@ import ( "git.kirsle.net/apps/doodle/pkg/wasm" ) +// Errors. +var ( + ErrNotFound = errors.New("file not found") +) + // ListDoodads returns a listing of all available doodads between all locations, // including user doodads. func ListDoodads() ([]string, error) { @@ -106,7 +113,20 @@ func ListBuiltin() ([]string, error) { return result, nil } +// LoadFromEmbeddable reads a doodad file, checking a level's embeddable +// file data in addition to the usual places. +func LoadFromEmbeddable(filename string, fs filesystem.Embeddable) (*Doodad, error) { + if bin, err := fs.GetFile(balance.EmbeddedDoodadsBasePath + filename); err == nil { + log.Debug("doodads.LoadFromEmbeddable: found %s", filename) + return Deserialize(filename, bin) + } + return LoadFile(filename) +} + // LoadFile reads a doodad file from disk, checking a few locations. +// +// It checks for embedded bindata, system-level doodads on the filesystem, +// and then user-owned doodads in their profile folder. func LoadFile(filename string) (*Doodad, error) { if !strings.HasSuffix(filename, enum.DoodadExt) { filename += enum.DoodadExt @@ -177,3 +197,24 @@ func (d *Doodad) WriteFile(filename string) error { return nil } + +// Serialize encodes a doodad to bytes and returns them, instead +// of writing to a file. +// WriteFile saves a doodad to disk in the user's config directory. +func (d *Doodad) Serialize() ([]byte, error) { + // Set the version information. + d.Version = 1 + d.GameVersion = branding.Version + + bin, err := d.ToJSON() + if err != nil { + return []byte{}, err + } + + return bin, nil +} + +// Deserialize loads a doodad from its bytes format. +func Deserialize(filename string, bin []byte) (*Doodad, error) { + return FromJSON(filename, bin) +} diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 1e04624..3780f31 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -279,6 +279,7 @@ func (s *EditorScene) LoadLevel(filename string) error { log.Info("Installing %d actors into the drawing", len(level.Actors)) if err := s.UI.Canvas.InstallActors(level.Actors); err != nil { + modal.Alert("This level references some doodads that were not found:\n\n%s", err).WithTitle("Level Errors") return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err) } diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index 01bb662..b356d22 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -28,7 +28,7 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) { if doodad == nil { if actor != nil { - obj, err := doodads.LoadFile(actor.Filename) + obj, err := doodads.LoadFromEmbeddable(actor.Filename, u.Scene.Level) if err != nil { log.Error("startDragExistingActor: actor doodad name %s not found: %s", actor.Filename, err) return diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 2011f87..4958e86 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -2,9 +2,13 @@ package doodle import ( "fmt" + "os" + "path/filepath" + "strings" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/level/publishing" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/modal" "git.kirsle.net/apps/doodle/pkg/windows" @@ -114,13 +118,27 @@ func (u *EditorUI) SetupPopups(d *Doodle) { Engine: d.Engine, Level: scene.Level, - OnPublish: func() { - modal.Alert("Not Yet Implemented") - // filename, err := native.SaveFile("Save your level", "*.level") - // if err != nil { - // modal.Alert(err.Error()) - // } - // log.Info("Write to: %s", filename) + OnPublish: func(includeBuiltins bool) { + log.Debug("OnPublish: include builtins=%+v", includeBuiltins) + cwd, _ := os.Getwd() + d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) { + if answer == "" { + d.Flash("A file name is required to publish this level.") + return + } + + if !strings.HasSuffix(answer, ".level") { + answer += ".level" + } + + answer = filepath.Join(cwd, answer) + log.Debug("call with includeBuiltins=%+v", includeBuiltins) + if _, err := publishing.Publish(scene.Level, answer, includeBuiltins); err != nil { + modal.Alert("Error when publishing the level: %s", err) + return + } + d.Flash("Exported published level to: %s", answer) + }) }, OnCancel: func() { u.publishWindow.Hide() diff --git a/pkg/level/filesystem.go b/pkg/level/filesystem.go index ab30d8c..13683c7 100644 --- a/pkg/level/filesystem.go +++ b/pkg/level/filesystem.go @@ -3,6 +3,7 @@ package level import ( "errors" "sort" + "strings" ) // FileSystem embeds a map of files inside a parent drawing. @@ -63,4 +64,18 @@ func (l *Level) ListFiles() []string { sort.Strings(files) return files -} \ No newline at end of file +} + +// ListFilesAt returns the list of files having a common prefix. +func (l *Level) ListFilesAt(prefix string) []string { + var ( + files = l.ListFiles() + match = []string{} + ) + for _, name := range files { + if strings.HasPrefix(name, prefix) { + match = append(match, name) + } + } + return match +} diff --git a/pkg/level/publishing/publishing.go b/pkg/level/publishing/publishing.go new file mode 100644 index 0000000..fe898a2 --- /dev/null +++ b/pkg/level/publishing/publishing.go @@ -0,0 +1,94 @@ +/* +Package publishing contains functionality for "publishing" a Level, which +involves the writing and reading of custom doodads embedded inside +the levels. + +Free tiers of the game will not read or write embedded doodads into +levels. +*/ +package publishing + +import ( + "fmt" + "sort" + + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" +) + +/* +Level "Publishing" functions, involving the writing and reading of embedded +doodads within level files. +*/ + +// Publish writes a published level file, with embedded doodads included. +func Publish(lvl *level.Level, filename string, includeBuiltins bool) (*level.Level, error) { + // Get and embed the doodads. + builtins, customs := GetUsedDoodadNames(lvl) + if includeBuiltins { + log.Debug("including builtins: %+v", builtins) + customs = append(customs, builtins...) + } + for _, filename := range customs { + log.Debug("Embed filename: %s", filename) + doodad, err := doodads.LoadFromEmbeddable(filename, lvl) + if err != nil { + return nil, fmt.Errorf("couldn't load doodad %s: %s", filename, err) + } + + bin, err := doodad.Serialize() + if err != nil { + return nil, fmt.Errorf("couldn't serialize doodad %s: %s", filename, err) + } + + // Embed it. + lvl.SetFile(balance.EmbeddedDoodadsBasePath+filename, bin) + } + + log.Info("Publish: write file to %s", filename) + err := lvl.WriteFile(filename) + return lvl, err +} + +// GetUsedDoodadNames returns the lists of doodad filenames in use in a level, +// bucketed by built-in or custom user doodads. +func GetUsedDoodadNames(lvl *level.Level) (builtin []string, custom []string) { + // Collect all the doodad names in use in this level. + unique := map[string]interface{}{} + names := []string{} + if lvl != nil { + for _, actor := range lvl.Actors { + if _, ok := unique[actor.Filename]; ok { + continue + } + unique[actor.Filename] = nil + names = append(names, actor.Filename) + } + } + + // Identify which of the doodads are built-ins. + // builtin = []string{} + builtinMap := map[string]interface{}{} + // custom := []string{} + if builtins, err := doodads.ListBuiltin(); err == nil { + for _, filename := range builtins { + if _, ok := unique[filename]; ok { + builtin = append(builtin, filename) + builtinMap[filename] = nil + } + } + } + for _, name := range names { + if _, ok := builtinMap[name]; ok { + continue + } + custom = append(custom, name) + } + + sort.Strings(builtin) + sort.Strings(custom) + + return builtin, custom +} diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 2255abc..f71cb52 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -150,12 +150,12 @@ func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) { w.Load(level.Palette, level.Chunker) // TODO: wallpaper paths - filename := "assets/wallpapers/" + level.Wallpaper + filename := balance.EmbeddedWallpaperBasePath + level.Wallpaper if runtime.GOOS != "js" { // Check if the wallpaper wasn't found. Check bindata and file system. if _, err := filesystem.FindFileEmbedded(filename, level); err != nil { log.Error("LoadLevel: wallpaper %s did not appear to exist, default to notebook.png", filename) - filename = "assets/wallpapers/notebook.png" + filename = balance.EmbeddedWallpaperBasePath + "notebook.png" } } diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 05892e7..12c1d48 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -2,7 +2,7 @@ package uix import ( "errors" - "fmt" + "strings" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" @@ -14,11 +14,14 @@ import ( // InstallActors adds external Actors to the canvas to be superimposed on top // of the drawing. func (w *Canvas) InstallActors(actors level.ActorMap) error { + var errs []string + w.actors = make([]*Actor, 0) for id, actor := range actors { - doodad, err := doodads.LoadFile(actor.Filename) + doodad, err := doodads.LoadFromEmbeddable(actor.Filename, w.level) if err != nil { - return fmt.Errorf("InstallActors: %s", err) + errs = append(errs, err.Error()) + continue } // Create the "live" Actor to exist in the world, and set its world @@ -28,6 +31,10 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error { w.actors = append(w.actors, liveActor) } + + if len(errs) > 0 { + return errors.New(strings.Join(errs, "\n")) + } return nil } diff --git a/pkg/windows/open_level_editor.go b/pkg/windows/open_level_editor.go index 05c4b48..f2753ec 100644 --- a/pkg/windows/open_level_editor.go +++ b/pkg/windows/open_level_editor.go @@ -189,41 +189,41 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window { } }(i, dd) } - } - // Browse button for local filesystem. - browseDoodadFrame := ui.NewFrame("Browse Doodad Frame") - frame.Pack(browseDoodadFrame, ui.Pack{ - Side: ui.N, - Expand: true, - FillX: true, - PadY: 1, - }) + // Browse button for local filesystem. + browseDoodadFrame := ui.NewFrame("Browse Doodad Frame") + frame.Pack(browseDoodadFrame, ui.Pack{ + Side: ui.N, + Expand: true, + FillX: true, + PadY: 1, + }) - browseDoodadButton := ui.NewButton("Browse Doodad", ui.NewLabel(ui.Label{ - Text: "Browse...", - Font: balance.MenuFont, - })) - browseDoodadButton.SetStyle(&balance.ButtonPrimary) - browseDoodadFrame.Pack(browseDoodadButton, ui.Pack{ - Side: ui.W, - }) + browseDoodadButton := ui.NewButton("Browse Doodad", ui.NewLabel(ui.Label{ + Text: "Browse...", + Font: balance.MenuFont, + })) + browseDoodadButton.SetStyle(&balance.ButtonPrimary) + browseDoodadFrame.Pack(browseDoodadButton, ui.Pack{ + Side: ui.W, + }) - browseDoodadButton.Handle(ui.Click, func(ed ui.EventData) error { - filename, err := native.OpenFile("Choose a .doodad file", "*.doodad") - if err != nil { - log.Error("Couldn't show file dialog: %s", err) + browseDoodadButton.Handle(ui.Click, func(ed ui.EventData) error { + filename, err := native.OpenFile("Choose a .doodad file", "*.doodad") + if err != nil { + log.Error("Couldn't show file dialog: %s", err) + return nil + } + + if config.LoadForPlay { + config.OnPlayLevel(filename) + } else { + config.OnEditLevel(filename) + } return nil - } - - if config.LoadForPlay { - config.OnPlayLevel(filename) - } else { - config.OnEditLevel(filename) - } - return nil - }) - config.Supervisor.Add(browseDoodadButton) + }) + config.Supervisor.Add(browseDoodadButton) + } /****************** * Confirm/cancel buttons. diff --git a/pkg/windows/publish_level.go b/pkg/windows/publish_level.go index 0de56b2..514e162 100644 --- a/pkg/windows/publish_level.go +++ b/pkg/windows/publish_level.go @@ -3,12 +3,11 @@ package windows import ( "fmt" "math" - "sort" "strings" "git.kirsle.net/apps/doodle/pkg/balance" - "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/level/publishing" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" @@ -21,7 +20,7 @@ type Publish struct { Engine render.Engine Level *level.Level - OnPublish func() + OnPublish func(builtinToo bool) OnCancel func() // Private vars. @@ -124,39 +123,40 @@ func NewPublishWindow(cfg Publish) *ui.Window { PadX: 2, }) - // Collect all the doodad names in use in this level. - unique := map[string]interface{}{} - names := []string{} - if cfg.Level != nil { - for _, actor := range cfg.Level.Actors { - if _, ok := unique[actor.Filename]; ok { - continue - } - unique[actor.Filename] = nil - names = append(names, actor.Filename) - } - } + // // Collect all the doodad names in use in this level. + // unique := map[string]interface{}{} + // names := []string{} + // if cfg.Level != nil { + // for _, actor := range cfg.Level.Actors { + // if _, ok := unique[actor.Filename]; ok { + // continue + // } + // unique[actor.Filename] = nil + // names = append(names, actor.Filename) + // } + // } - sort.Strings(names) + // sort.Strings(names) - // Identify which of the doodads are built-ins. - usedBuiltins := []string{} - builtinMap := map[string]interface{}{} - usedCustom := []string{} - if builtins, err := doodads.ListBuiltin(); err == nil { - for _, filename := range builtins { - if _, ok := unique[filename]; ok { - usedBuiltins = append(usedBuiltins, filename) - builtinMap[filename] = nil - } - } - } - for _, name := range names { - if _, ok := builtinMap[name]; ok { - continue - } - usedCustom = append(usedCustom, name) - } + // // Identify which of the doodads are built-ins. + // usedBuiltins := []string{} + // builtinMap := map[string]interface{}{} + // usedCustom := []string{} + // if builtins, err := doodads.ListBuiltin(); err == nil { + // for _, filename := range builtins { + // if _, ok := unique[filename]; ok { + // usedBuiltins = append(usedBuiltins, filename) + // builtinMap[filename] = nil + // } + // } + // } + // for _, name := range names { + // if _, ok := builtinMap[name]; ok { + // continue + // } + // usedCustom = append(usedCustom, name) + // } + usedBuiltins, usedCustom := publishing.GetUsedDoodadNames(cfg.Level) // Helper function to draw the button rows for a set of doodads. mkDoodadRows := func(filenames []string, builtin bool) []*ui.Frame { @@ -201,7 +201,7 @@ func NewPublishWindow(cfg Publish) *ui.Window { builtinRows = []*ui.Frame{} customRows = []*ui.Frame{} ) - if len(names) > 0 { + if len(usedCustom) > 0 { customRows = mkDoodadRows(usedCustom, false) btnRows = append(btnRows, customRows...) } @@ -284,7 +284,7 @@ func NewPublishWindow(cfg Publish) *ui.Window { }{ {"Export Level", true, func() { if cfg.OnPublish != nil { - cfg.OnPublish() + cfg.OnPublish(cfg.includeBuiltins) } }}, {"Close", false, func() {