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.
This commit is contained in:
Noah 2021-09-11 21:18:22 -07:00
parent ecdfc46358
commit 0a8bce708e
14 changed files with 244 additions and 25 deletions

View File

@ -125,7 +125,7 @@ func main() {
} }
// Enable feature flags? // Enable feature flags?
if c.Bool("experimental") { if c.Bool("experimental") || usercfg.Current.EnableFeatures {
balance.FeaturesOn() balance.FeaturesOn()
} }

View File

@ -2,10 +2,17 @@ package balance
// Feature Flags to turn on/off experimental content. // Feature Flags to turn on/off experimental content.
var Feature = feature{ 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 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. // Allow embedded doodads in levels.
EmbeddableDoodads: true, EmbeddableDoodads: true,
} }
@ -13,13 +20,12 @@ var Feature = feature{
// FeaturesOn turns on all feature flags, from CLI --experimental option. // FeaturesOn turns on all feature flags, from CLI --experimental option.
func FeaturesOn() { func FeaturesOn() {
Feature.Zoom = true Feature.Zoom = true
Feature.CustomWallpaper = true
Feature.ChangePalette = true Feature.ChangePalette = true
} }
type feature struct { type feature struct {
Zoom bool Zoom bool
CustomWallpaper bool CustomWallpaper bool
ChangePalette bool ChangePalette bool
EmbeddableDoodads bool EmbeddableDoodads bool
} }

View File

@ -239,6 +239,7 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window {
DebugOverlay: &DebugOverlay, DebugOverlay: &DebugOverlay,
DebugCollision: &DebugCollision, DebugCollision: &DebugCollision,
HorizontalToolbars: &usercfg.Current.HorizontalToolbars, HorizontalToolbars: &usercfg.Current.HorizontalToolbars,
EnableFeatures: &usercfg.Current.EnableFeatures,
} }
return windows.MakeSettingsWindow(d.width, d.height, cfg) return windows.MakeSettingsWindow(d.width, d.height, cfg)
} }

View File

@ -77,6 +77,23 @@ func (s *EditorScene) Setup(d *Doodle) error {
return nil 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, // setupAsync initializes trhe editor scene in the background,
// underneath a loading screen. // underneath a loading screen.
func (s *EditorScene) setupAsync(d *Doodle) error { func (s *EditorScene) setupAsync(d *Doodle) error {

View File

@ -347,6 +347,13 @@ func (u *EditorUI) Present(e render.Engine) {
if u.Supervisor.IsDragging() { if u.Supervisor.IsDragging() {
if actor := u.DraggableActor; actor != nil { if actor := u.DraggableActor; actor != nil {
var size = actor.canvas.Size() 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( actor.canvas.Present(u.d.Engine, render.NewPoint(
u.cursor.X-(size.W/2), u.cursor.X-(size.W/2),
u.cursor.Y-(size.H/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 // NOTE: The drag event begins at editor_ui_doodad.go when configuring the
// Doodad Palette buttons. // Doodad Palette buttons.
drawing.Handle(ui.Drop, func(ed ui.EventData) error { 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) var P = ui.AbsolutePosition(drawing)
// Was it an actor from the Doodad Palette? // 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? // Was it an already existing actor to re-add to the map?
if actor.actor != nil { if actor.actor != nil {
actor.actor.Point = position actor.actor.Point = position

View File

@ -133,6 +133,10 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
scene.Level.Wallpaper = wallpaper scene.Level.Wallpaper = wallpaper
u.Canvas.LoadLevel(scene.Level) u.Canvas.LoadLevel(scene.Level)
}, },
OnReload: func() {
log.Warn("RELOAD LEVEL")
scene.Reset()
},
OnCancel: func() { OnCancel: func() {
u.levelSettingsWindow.Hide() u.levelSettingsWindow.Hide()
}, },

View File

@ -62,10 +62,6 @@ func LoadBinary(filename string) (*Level, error) {
} }
// 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.Inflate()
m.Actors.Inflate()
// Inflate the private instance values.
m.Palette.Inflate()
return m, err return m, err
} }

View File

@ -42,11 +42,7 @@ func FromJSON(filename string, data []byte) (*Level, error) {
} }
// 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.Inflate()
m.Actors.Inflate()
// Inflate the private instance values.
m.Palette.Inflate()
return m, nil return m, nil
} }

24
pkg/level/inflate.go Normal file
View File

@ -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()
}

View File

@ -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
}
}

View File

@ -116,8 +116,18 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) {
can = a.Canvas // Canvas widget that draws the actor can = a.Canvas // Canvas widget that draws the actor
actorPoint = a.Position() actorPoint = a.Position()
actorSize = a.Size() 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 // Create a box of World Coordinates that this actor occupies. The
// Actor X,Y from level data is already a World Coordinate; // Actor X,Y from level data is already a World Coordinate;
// accomodate for the size of the Actor. // 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), X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1),
Y: p.Y + w.Scroll.Y + actorPoint.Y + 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, // XXX TODO: when an Actor hits the left or top edge and shrinks,
// scrolling to offset that shrink is currently hard to solve. // scrolling to offset that shrink is currently hard to solve.

View File

@ -29,6 +29,7 @@ type Settings struct {
// Configurable settings (pkg/windows/settings.go) // Configurable settings (pkg/windows/settings.go)
HorizontalToolbars bool `json:",omitempty"` HorizontalToolbars bool `json:",omitempty"`
EnableFeatures bool `json:",omitempty"`
// Secret boolprops from balance/boolprops.go // Secret boolprops from balance/boolprops.go
ShowHiddenDoodads bool `json:",omitempty"` ShowHiddenDoodads bool `json:",omitempty"`

View File

@ -22,6 +22,7 @@ type AddEditLevel struct {
// Callback functions. // Callback functions.
OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string) OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string)
OnCreateNewLevel func(*level.Level) OnCreateNewLevel func(*level.Level)
OnReload func()
OnCancel func() OnCancel func()
} }
@ -411,7 +412,11 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
"if the new palette is smaller, some pixels may be\n" + "if the new palette is smaller, some pixels may be\n" +
"lost from your level. OK to continue?", "lost from your level. OK to continue?",
).WithTitle("Change Level Palette").Then(func() { ).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 return nil
} }

View File

@ -23,6 +23,7 @@ type Settings struct {
DebugOverlay *bool DebugOverlay *bool
DebugCollision *bool DebugCollision *bool
HorizontalToolbars *bool HorizontalToolbars *bool
EnableFeatures *bool
// Configuration options. // Configuration options.
SceneName string // name of scene which called this window SceneName string // name of scene which called this window
@ -71,13 +72,10 @@ func NewSettingsWindow(cfg Settings) *ui.Window {
FillX: true, FillX: true,
}) })
/////////// // Make the tabs
// Options (index) Tab
cfg.makeOptionsTab(tabFrame, Width, Height) cfg.makeOptionsTab(tabFrame, Width, Height)
///////////
// Controls Tab
cfg.makeControlsTab(tabFrame, Width, Height) cfg.makeControlsTab(tabFrame, Width, Height)
cfg.makeExperimentalTab(tabFrame, Width, Height)
tabFrame.Supervise(cfg.Supervisor) tabFrame.Supervise(cfg.Supervisor)
@ -455,3 +453,129 @@ func (c Settings) makeControlsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.
return frame 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
}