From 0a8bce708e6f3d81c3ad222abf40624b6c538b67 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 11 Sep 2021 21:18:22 -0700 Subject: [PATCH] Actor Zoom + Experimental Settings GUI Improvements to the Zoom feature: * Actor position and size within your level scales up and down appropriately. The canvas size of the actor is scaled and its canvas is told the Zoom number of the parent so it will render its own graphic scaled correctly too. Other features: * "Experimental" tab added to the Settings window as a UI version of the --experimental CLI option. The option saves persistently to disk. * The "Replace Palette" experimental feature now works better. Debating whether it's a useful feature to even have. --- cmd/doodle/main.go | 2 +- pkg/balance/feature_flags.go | 18 +++-- pkg/doodle.go | 1 + pkg/editor_scene.go | 17 +++++ pkg/editor_ui.go | 13 +++- pkg/editor_ui_popups.go | 4 + pkg/level/fmt_binary.go | 6 +- pkg/level/fmt_json.go | 6 +- pkg/level/inflate.go | 24 ++++++ pkg/level/palette.go | 25 +++++++ pkg/uix/canvas_actors.go | 11 ++- pkg/usercfg/usercfg.go | 1 + pkg/windows/add_edit_level.go | 7 +- pkg/windows/settings.go | 134 ++++++++++++++++++++++++++++++++-- 14 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 pkg/level/inflate.go diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 40bf9ff..d256efd 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -125,7 +125,7 @@ func main() { } // Enable feature flags? - if c.Bool("experimental") { + if c.Bool("experimental") || usercfg.Current.EnableFeatures { balance.FeaturesOn() } diff --git a/pkg/balance/feature_flags.go b/pkg/balance/feature_flags.go index e7f5ad0..e00fc5b 100644 --- a/pkg/balance/feature_flags.go +++ b/pkg/balance/feature_flags.go @@ -2,10 +2,17 @@ package balance // Feature Flags to turn on/off experimental content. var Feature = feature{ - Zoom: false, // enable the zoom in/out feature (very buggy rn) - CustomWallpaper: true, // attach custom wallpaper img to levels + ///////// + // Experimental features that are off by default + Zoom: false, // enable the zoom in/out feature (very buggy rn) ChangePalette: false, // reset your palette after level creation to a diff preset + ///////// + // Fully activated features + + // Attach custom wallpaper img to levels + CustomWallpaper: true, + // Allow embedded doodads in levels. EmbeddableDoodads: true, } @@ -13,13 +20,12 @@ var Feature = feature{ // FeaturesOn turns on all feature flags, from CLI --experimental option. func FeaturesOn() { Feature.Zoom = true - Feature.CustomWallpaper = true Feature.ChangePalette = true } type feature struct { - Zoom bool - CustomWallpaper bool - ChangePalette bool + Zoom bool + CustomWallpaper bool + ChangePalette bool EmbeddableDoodads bool } diff --git a/pkg/doodle.go b/pkg/doodle.go index 9e0e816..3219bd1 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -239,6 +239,7 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window { DebugOverlay: &DebugOverlay, DebugCollision: &DebugCollision, HorizontalToolbars: &usercfg.Current.HorizontalToolbars, + EnableFeatures: &usercfg.Current.EnableFeatures, } return windows.MakeSettingsWindow(d.width, d.height, cfg) } diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 480de4f..2c7407e 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -77,6 +77,23 @@ func (s *EditorScene) Setup(d *Doodle) error { return nil } +// Reset the editor scene from scratch. Good nuclear option when you change the level's +// palette on-the-fly or some other sticky situation and want to reload the editor. +func (s *EditorScene) Reset() { + if s.Level != nil { + s.Level.Chunker.Redraw() + } + if s.Doodad != nil { + s.Doodad.Layers[s.ActiveLayer].Chunker.Redraw() + } + + s.d.Goto(&EditorScene{ + Filename: s.Filename, + Level: s.Level, + Doodad: s.Doodad, + }) +} + // setupAsync initializes trhe editor scene in the background, // underneath a loading screen. func (s *EditorScene) setupAsync(d *Doodle) error { diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index ca2fcc3..ca5e656 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -347,6 +347,13 @@ func (u *EditorUI) Present(e render.Engine) { if u.Supervisor.IsDragging() { if actor := u.DraggableActor; actor != nil { var size = actor.canvas.Size() + + // Scale the actor up or down by the zoom level of the Editor Canvas. + // TODO: didn't seem to make a difference + // size.W = u.Canvas.ZoomMultiply(size.W) + // size.H = u.Canvas.ZoomMultiply(size.H) + // actor.canvas.Zoom = u.Canvas.Zoom + actor.canvas.Present(u.d.Engine, render.NewPoint( u.cursor.X-(size.W/2), u.cursor.Y-(size.H/2), @@ -415,7 +422,7 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { // NOTE: The drag event begins at editor_ui_doodad.go when configuring the // Doodad Palette buttons. drawing.Handle(ui.Drop, func(ed ui.EventData) error { - log.Info("Drawing canvas has received a drop!") + // Editor Canvas's position relative to the window. var P = ui.AbsolutePosition(drawing) // Was it an actor from the Doodad Palette? @@ -446,6 +453,10 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { } ) + // Adjust the level position per the zoom factor. + position.X = drawing.ZoomDivide(position.X) + position.Y = drawing.ZoomDivide(position.Y) + // Was it an already existing actor to re-add to the map? if actor.actor != nil { actor.actor.Point = position diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 87972bc..82cf78e 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -133,6 +133,10 @@ func (u *EditorUI) SetupPopups(d *Doodle) { scene.Level.Wallpaper = wallpaper u.Canvas.LoadLevel(scene.Level) }, + OnReload: func() { + log.Warn("RELOAD LEVEL") + scene.Reset() + }, OnCancel: func() { u.levelSettingsWindow.Hide() }, diff --git a/pkg/level/fmt_binary.go b/pkg/level/fmt_binary.go index d4293a2..d226b17 100644 --- a/pkg/level/fmt_binary.go +++ b/pkg/level/fmt_binary.go @@ -62,10 +62,6 @@ func LoadBinary(filename string) (*Level, error) { } // Inflate the chunk metadata to map the pixels to their palette indexes. - m.Chunker.Inflate(m.Palette) - m.Actors.Inflate() - - // Inflate the private instance values. - m.Palette.Inflate() + m.Inflate() return m, err } diff --git a/pkg/level/fmt_json.go b/pkg/level/fmt_json.go index 2474a0b..ca64118 100644 --- a/pkg/level/fmt_json.go +++ b/pkg/level/fmt_json.go @@ -42,11 +42,7 @@ func FromJSON(filename string, data []byte) (*Level, error) { } // Inflate the chunk metadata to map the pixels to their palette indexes. - m.Chunker.Inflate(m.Palette) - m.Actors.Inflate() - - // Inflate the private instance values. - m.Palette.Inflate() + m.Inflate() return m, nil } diff --git a/pkg/level/inflate.go b/pkg/level/inflate.go new file mode 100644 index 0000000..7f60aed --- /dev/null +++ b/pkg/level/inflate.go @@ -0,0 +1,24 @@ +package level + +/* +Inflate the level from its compressed JSON form. + +This is called as part of the LoadJSON function when the level is read +from disk. In the JSON format, pixels in the level refer to the palette +by its index number. + +This function calls the following: + +* Chunker.Inflate(Palette) to update references to the level's pixels to point + to the Swatch entry. +* Actors.Inflate() +* Palette.Inflate() to load private instance values for the palette subsystem. +*/ +func (l *Level) Inflate() { + // Inflate the chunk metadata to map the pixels to their palette indexes. + l.Chunker.Inflate(l.Palette) + l.Actors.Inflate() + + // Inflate the private instance values. + l.Palette.Inflate() +} diff --git a/pkg/level/palette.go b/pkg/level/palette.go index 2dc29c6..6dc8b9a 100644 --- a/pkg/level/palette.go +++ b/pkg/level/palette.go @@ -134,3 +134,28 @@ func (p *Palette) update() { } } } + +// ReplacePalette installs a new palette into your level. +// Your existing level colors, by index, are replaced by the incoming +// palette. If the new palette is smaller, extraneous indices are +// left alone. +func (l *Level) ReplacePalette(pal *Palette) { + for i, swatch := range pal.Swatches { + if i >= len(l.Palette.Swatches) { + l.Palette.Swatches = append(l.Palette.Swatches, swatch) + continue + } + + // Ugly code, but can't just replace the swatch + // wholesale -- the inflated level data means existing + // pixels already have refs to their Swatch and they + // will keep those refs until you fully save and exit + // out of the editor. + l.Palette.Swatches[i].Name = swatch.Name + l.Palette.Swatches[i].Color = swatch.Color + l.Palette.Swatches[i].Pattern = swatch.Pattern + l.Palette.Swatches[i].Solid = swatch.Solid + l.Palette.Swatches[i].Fire = swatch.Fire + l.Palette.Swatches[i].Water = swatch.Water + } +} diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 8d99216..32ad18a 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -116,8 +116,18 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) { can = a.Canvas // Canvas widget that draws the actor actorPoint = a.Position() actorSize = a.Size() + resizeTo = actorSize ) + // Adjust actor position and size by the zoom level. + actorPoint.X = w.ZoomMultiply(actorPoint.X) + actorPoint.Y = w.ZoomMultiply(actorPoint.Y) + resizeTo.W = w.ZoomMultiply(resizeTo.W) + resizeTo.H = w.ZoomMultiply(resizeTo.H) + + // Tell the actor's canvas to copy our zoom level so it resizes its image too. + can.Zoom = w.Zoom + // Create a box of World Coordinates that this actor occupies. The // Actor X,Y from level data is already a World Coordinate; // accomodate for the size of the Actor. @@ -137,7 +147,6 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) { X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1), Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1), } - resizeTo := actorSize // XXX TODO: when an Actor hits the left or top edge and shrinks, // scrolling to offset that shrink is currently hard to solve. diff --git a/pkg/usercfg/usercfg.go b/pkg/usercfg/usercfg.go index cccd3ef..802da9b 100644 --- a/pkg/usercfg/usercfg.go +++ b/pkg/usercfg/usercfg.go @@ -29,6 +29,7 @@ type Settings struct { // Configurable settings (pkg/windows/settings.go) HorizontalToolbars bool `json:",omitempty"` + EnableFeatures bool `json:",omitempty"` // Secret boolprops from balance/boolprops.go ShowHiddenDoodads bool `json:",omitempty"` diff --git a/pkg/windows/add_edit_level.go b/pkg/windows/add_edit_level.go index 67f27a3..fb60cc3 100644 --- a/pkg/windows/add_edit_level.go +++ b/pkg/windows/add_edit_level.go @@ -22,6 +22,7 @@ type AddEditLevel struct { // Callback functions. OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string) OnCreateNewLevel func(*level.Level) + OnReload func() OnCancel func() } @@ -411,7 +412,11 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { "if the new palette is smaller, some pixels may be\n" + "lost from your level. OK to continue?", ).WithTitle("Change Level Palette").Then(func() { - config.OnCancel() + // Install the new level palette. + config.EditLevel.ReplacePalette(level.DefaultPalettes[paletteName]) + if config.OnReload != nil { + config.OnReload() + } }) return nil } diff --git a/pkg/windows/settings.go b/pkg/windows/settings.go index 7a7d076..91c7768 100644 --- a/pkg/windows/settings.go +++ b/pkg/windows/settings.go @@ -23,6 +23,7 @@ type Settings struct { DebugOverlay *bool DebugCollision *bool HorizontalToolbars *bool + EnableFeatures *bool // Configuration options. SceneName string // name of scene which called this window @@ -71,13 +72,10 @@ func NewSettingsWindow(cfg Settings) *ui.Window { FillX: true, }) - /////////// - // Options (index) Tab + // Make the tabs cfg.makeOptionsTab(tabFrame, Width, Height) - - /////////// - // Controls Tab cfg.makeControlsTab(tabFrame, Width, Height) + cfg.makeExperimentalTab(tabFrame, Width, Height) tabFrame.Supervise(cfg.Supervisor) @@ -455,3 +453,129 @@ func (c Settings) makeControlsTab(tabFrame *ui.TabFrame, Width, Height int) *ui. return frame } + +// Settings Window "Experimental" Tab +func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { + tab := tabFrame.AddTab("Experimental", ui.NewLabel(ui.Label{ + Text: "Experimental", + Font: balance.TabFont, + })) + tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) + + // Common click handler for all settings, + // so we can write the updated info to disk. + onClick := func(ed ui.EventData) error { + saveGameSettings() + return nil + } + + rows := []struct { + Header string + Text string + Boolean *bool + TextVariable *string + PadY int + PadX int + name string // for special cases + }{ + { + Header: "Enable Experimental Features", + }, + { + Text: "The setting below can enable experimental features in this\n" + + "game. These are features which are still in development and\n" + + "may have unstable or buggy behavior.", + PadY: 2, + }, + { + Header: "Zoom In/Out", + }, + { + Text: "This adds Zoom options to the level editor. It has a few\n" + + "bugs around scrolling but may be useful already.", + PadY: 2, + }, + { + Header: "Replace Level Palette", + }, + { + Text: "This adds an option to the Level Properties dialog to\n" + + "replace your level palette with one of the defaults,\n" + + "like on the New Level screen. It might not actually work.", + PadY: 2, + }, + { + Boolean: c.EnableFeatures, + Text: "Enable experimental features", + PadX: 4, + }, + { + Text: "Restart the game for changes to take effect.", + PadY: 2, + }, + } + for _, row := range rows { + row := row + frame := ui.NewFrame("Frame") + tab.Pack(frame, ui.Pack{ + Side: ui.N, + FillX: true, + PadY: row.PadY, + }) + + // Headers get their own row to themselves. + if row.Header != "" { + label := ui.NewLabel(ui.Label{ + Text: row.Header, + Font: balance.LabelFont, + }) + frame.Pack(label, ui.Pack{ + Side: ui.W, + PadX: row.PadX, + }) + continue + } + + // Checkboxes get their own row. + if row.Boolean != nil { + cb := ui.NewCheckbox(row.Text, row.Boolean, ui.NewLabel(ui.Label{ + Text: row.Text, + Font: balance.UIFont, + })) + cb.Handle(ui.Click, onClick) + cb.Supervise(c.Supervisor) + + // Add warning to the toolbars option if the EditMode is currently active. + if row.name == "toolbars" && c.SceneName == "Edit" { + ui.NewTooltip(cb, ui.Tooltip{ + Text: "Note: reload your level after changing this option.\n" + + "Playtesting and returning will do.", + Edge: ui.Top, + }) + } + + frame.Pack(cb, ui.Pack{ + Side: ui.W, + PadX: row.PadX, + }) + continue + } + + // Any leftover Text gets packed to the left. + if row.Text != "" { + tf := ui.NewFrame("TextFrame") + label := ui.NewLabel(ui.Label{ + Text: row.Text, + Font: balance.UIFont, + }) + tf.Pack(label, ui.Pack{ + Side: ui.W, + }) + frame.Pack(tf, ui.Pack{ + Side: ui.W, + }) + } + } + + return tab +}