diff --git a/assets/sprites/gear.png b/assets/sprites/gear.png new file mode 100644 index 0000000..50e378f Binary files /dev/null and b/assets/sprites/gear.png differ diff --git a/cmd/doodad/commands/edit_doodad.go b/cmd/doodad/commands/edit_doodad.go index 61d6353..e150d79 100644 --- a/cmd/doodad/commands/edit_doodad.go +++ b/cmd/doodad/commands/edit_doodad.go @@ -43,6 +43,11 @@ func init() { Aliases: []string{"t"}, Usage: "set a key/value tag on the doodad, in key=value format. Empty value deletes the tag.", }, + &cli.StringFlag{ + Name: "option", + Aliases: []string{"o"}, + Usage: "set an option on the doodad, in key=type=default format, e.g. active=bool=true, speed=int=10, name=str. Value types are bool, str, int.", + }, &cli.BoolFlag{ Name: "hide", Usage: "Hide the doodad from the palette", @@ -167,6 +172,36 @@ func editDoodad(c *cli.Context, filename string) error { modified = true } + // Options. + opt := c.String("option") + if len(opt) > 0 { + parts := strings.SplitN(opt, "=", 3) + if len(parts) < 2 { + log.Error("--option: must be in format `name=type` or `name=type=value`") + os.Exit(1) + } + + var ( + name = parts[0] + dataType = parts[1] + value string + ) + if len(parts) == 3 { + value = parts[2] + } + + // Validate the data types. + if dataType != "bool" && dataType != "str" && dataType != "int" { + log.Error("--option: invalid type, should be a bool, str or int") + os.Exit(1) + } + + value = dd.SetOption(name, dataType, value) + log.Info("Set option %s (%s) = %s", name, dataType, value) + + modified = true + } + if c.Bool("hide") { dd.Hidden = true log.Info("Marked doodad Hidden") diff --git a/cmd/doodad/commands/show.go b/cmd/doodad/commands/show.go index 580b1bf..113e2bf 100644 --- a/cmd/doodad/commands/show.go +++ b/cmd/doodad/commands/show.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "path/filepath" + "sort" "strings" "git.kirsle.net/SketchyMaze/doodle/pkg/doodads" @@ -143,6 +144,19 @@ func showLevel(c *cli.Context, filename string) error { fmt.Printf(" - Name: %s\n", actor.Filename) fmt.Printf(" UUID: %s\n", id) fmt.Printf(" At: %s\n", actor.Point) + if len(actor.Options) > 0 { + var ordered = []string{} + for name := range actor.Options { + ordered = append(ordered, name) + } + sort.Strings(ordered) + + fmt.Println(" Options:") + for _, name := range ordered { + val := actor.Options[name] + fmt.Printf(" %s %s = %v\n", val.Type, val.Name, val.Value) + } + } if c.Bool("links") { for _, link := range actor.Links { if other, ok := lvl.Actors[link]; ok { @@ -205,6 +219,21 @@ func showDoodad(c *cli.Context, filename string) error { fmt.Println("") } + if len(dd.Options) > 0 { + var ordered = []string{} + for name := range dd.Options { + ordered = append(ordered, name) + } + sort.Strings(ordered) + + fmt.Println("Options:") + for _, name := range ordered { + opt := dd.Options[name] + fmt.Printf(" %s %s = %v\n", opt.Type, opt.Name, opt.Default) + } + fmt.Println("") + } + showPalette(dd.Palette) for i, layer := range dd.Layers { diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index da4444f..9f6e480 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -148,4 +148,12 @@ var ( var ( // Number of Doodads per row in the palette. UIDoodadsPerRow = 2 + + // Size of the DoodadButtons on actor canvas mouseover. + UICanvasDoodadButtonSize = 16 + + // Threshold for "very small doodad" where the buttons take up too big a proportion + // and the doodad can't drag/drop easily.. tiny doodads will show the DoodadButtons + // 50% off the top/right edge. + UICanvasDoodadButtonSpaceNeeded = 20 ) diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index e8a6f3e..a39bf94 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -14,6 +14,7 @@ var ( GoldCoin = "assets/sprites/gold.png" SilverCoin = "assets/sprites/silver.png" LockIcon = "assets/sprites/padlock.png" + GearIcon = "assets/sprites/gear.png" // Cursors CursorIcon = "assets/sprites/pointer.png" diff --git a/pkg/doodads/doodad.go b/pkg/doodads/doodad.go index e9819cf..0a39dc5 100644 --- a/pkg/doodads/doodad.go +++ b/pkg/doodads/doodad.go @@ -11,13 +11,14 @@ import ( // Doodad is a reusable component for Levels that have scripts and graphics. type Doodad struct { level.Base - Filename string `json:"-"` // used internally, not saved in json - Hidden bool `json:"hidden,omitempty"` - Palette *level.Palette `json:"palette"` - Script string `json:"script"` - Hitbox render.Rect `json:"hitbox"` - Layers []Layer `json:"layers"` - Tags map[string]string `json:"data"` // arbitrary key/value data storage + Filename string `json:"-"` // used internally, not saved in json + Hidden bool `json:"hidden,omitempty"` + Palette *level.Palette `json:"palette"` + Script string `json:"script"` + Hitbox render.Rect `json:"hitbox"` + Layers []Layer `json:"layers"` + Tags map[string]string `json:"data"` // arbitrary key/value data storage + Options map[string]*Option `json:"options"` // runtime options for a doodad // Undo history, temporary live data not persisted to the level file. UndoHistory *drawtool.History `json:"-"` @@ -48,6 +49,7 @@ func New(size int) *Doodad { }, }, Tags: map[string]string{}, + Options: map[string]*Option{}, UndoHistory: drawtool.NewHistory(balance.UndoHistory), } } diff --git a/pkg/doodads/options.go b/pkg/doodads/options.go new file mode 100644 index 0000000..f2552e6 --- /dev/null +++ b/pkg/doodads/options.go @@ -0,0 +1,47 @@ +package doodads + +import ( + "fmt" + "strconv" + + "git.kirsle.net/SketchyMaze/doodle/pkg/log" +) + +// Options for runtime, user configurable. +type Option struct { + Type string // bool, str, int + Name string + Default interface{} +} + +// SetOption sets an actor option, safely. +func (d *Doodad) SetOption(name, dataType, v string) string { + if _, ok := d.Options[name]; !ok { + d.Options[name] = &Option{ + Type: dataType, + Name: name, + } + } + + return d.Options[name].Set(v) +} + +// Set an option value. Generally do not call this yourself - use SetOption +// to safely set an option which will create the map value the first time. +func (o *Option) Set(v string) string { + switch o.Type { + case "bool": + o.Default = v == "true" + case "str": + o.Default = v + case "int": + if val, err := strconv.Atoi(v); err != nil { + log.Error("Doodad Option.Set: not an int: %v", val) + } else { + o.Default = val + } + default: + log.Error("Doodad Option.Set: don't know how to set a %s type", o.Type) + } + return fmt.Sprintf("%v", o.Default) +} diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 51dc694..75b2c34 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -15,6 +15,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" "git.kirsle.net/SketchyMaze/doodle/pkg/uix" "git.kirsle.net/SketchyMaze/doodle/pkg/usercfg" + "git.kirsle.net/SketchyMaze/doodle/pkg/windows" "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" "git.kirsle.net/go/ui" @@ -56,6 +57,7 @@ type EditorUI struct { filesystemWindow *ui.Window licenseWindow *ui.Window settingsWindow *ui.Window // lazy loaded + doodadConfigWindows map[string]*ui.Window // Palette window. Palette *ui.Window @@ -72,13 +74,14 @@ type EditorUI struct { // NewEditorUI initializes the Editor UI. func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { u := &EditorUI{ - d: d, - Scene: s, - Supervisor: ui.NewSupervisor(), - StatusMouseText: "Cursor: (waiting)", - StatusPaletteText: "Swatch: ", - StatusFilenameText: "Filename: ", - StatusScrollText: "Hello world", + d: d, + Scene: s, + Supervisor: ui.NewSupervisor(), + StatusMouseText: "Cursor: (waiting)", + StatusPaletteText: "Swatch: ", + StatusFilenameText: "Filename: ", + StatusScrollText: "Hello world", + doodadConfigWindows: map[string]*ui.Window{}, } // The screen is a full-window-sized frame for laying out the UI. @@ -439,6 +442,24 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { d.Flash("Linked '%s' and '%s' together", a.Doodad().Title, b.Doodad().Title) } + drawing.OnDoodadConfig = func(a *uix.Actor) { + if win, ok := u.doodadConfigWindows[a.ID()]; ok { + win.Show() + } else { + win = windows.NewDoodadConfigWindow(&windows.DoodadConfig{ + Supervisor: u.Supervisor, + Engine: d.Engine, + EditActor: a, + OnRefresh: func() { + + }, + }) + u.ConfigureWindow(d, win) + win.Show() + u.doodadConfigWindows[a.ID()] = win + } + } + // Set up the drop handler for draggable doodads. // NOTE: The drag event begins at editor_ui_doodad.go when configuring the // Doodad Palette buttons. @@ -494,10 +515,10 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { actor.actor.Point = position u.Scene.Level.Actors.Add(actor.actor) } else { - u.Scene.Level.Actors.Add(&level.Actor{ + u.Scene.Level.Actors.Add(level.NewActor(level.Actor{ Point: position, Filename: actor.doodad.Filename, - }) + })) } err := drawing.InstallActors(u.Scene.Level.Actors) diff --git a/pkg/level/actor_options.go b/pkg/level/actor_options.go new file mode 100644 index 0000000..8200639 --- /dev/null +++ b/pkg/level/actor_options.go @@ -0,0 +1,48 @@ +package level + +import ( + "fmt" + "strconv" + + "git.kirsle.net/SketchyMaze/doodle/pkg/log" +) + +// Option for runtime, user configurable overrides of Doodad Options. +type Option struct { + Type string // bool, str, int + Name string + Value interface{} +} + +// SetOption sets an actor option, safely. +func (a *Actor) SetOption(name, dataType, v string) string { + if _, ok := a.Options[name]; !ok { + a.Options[name] = &Option{ + Type: dataType, + Name: name, + } + } + + return a.Options[name].Set(v) +} + +// Set an option value. Generally do not call this yourself - use SetOption +// to safely set an option which will create the map value the first time. +func (o *Option) Set(v string) string { + switch o.Type { + case "bool": + o.Value = v == "true" + case "str": + o.Value = v + case "int": + if val, err := strconv.Atoi(v); err != nil { + log.Error("Actor Option.Set: not an int: %v", val) + o.Value = 0 + } else { + o.Value = val + } + default: + log.Error("Actor Option.Set: don't know how to set a %s type", o.Type) + } + return fmt.Sprintf("%v", o.Value) +} diff --git a/pkg/level/actors.go b/pkg/level/actors.go index 6fd7567..70ecdbd 100644 --- a/pkg/level/actors.go +++ b/pkg/level/actors.go @@ -40,6 +40,17 @@ type Actor struct { Filename string `json:"filename"` // like "exit.doodad" Point render.Point `json:"point"` Links []string `json:"links,omitempty"` // IDs of linked actors + Options map[string]*Option +} + +// NewActor initializes a level.Actor. +func NewActor(a Actor) *Actor { + return &Actor{ + Filename: a.Filename, + Point: a.Point, + Links: []string{}, + Options: map[string]*Option{}, + } } // ID returns the actor's ID. diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 7632475..86d9829 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -43,8 +43,7 @@ type Actor struct { frozen bool // Frozen, via Freeze() and Unfreeze() immortal bool // Invulnerable to damage hitbox render.Rect - inventory map[string]int // item inventory. doodad name -> quantity, 0 for key item. - data map[string]string // arbitrary key/value store. DEPRECATED ?? + inventory map[string]int // item inventory. doodad name -> quantity, 0 for key item. // Movement data. position render.Point @@ -357,30 +356,34 @@ func (a *Actor) Hitbox() render.Rect { return a.hitbox } -// SetData sets an arbitrary field in the actor's K/V storage. -func (a *Actor) SetData(key, value string) { - if a.data == nil { - a.data = map[string]string{} +// Options returns the list of all available Doodad options, sorted. +func (a *Actor) Options() []string { + var result = []string{} + for option := range a.Doodad().Options { + result = append(result, option) } - - a.muData.Lock() - a.data[key] = value - a.muData.Unlock() + sort.Strings(result) + return result } -// GetData gets an arbitrary field from the actor's K/V storage. -// Missing keys just return a blank string (friendly to the JavaScript -// environment). -func (a *Actor) GetData(key string) string { - if a.data == nil { - return "" +// Get an option value from the actor. If the option is not configured, +// returns the default Doodad option, or nil if not there either. +func (a *Actor) GetOption(name string) *level.Option { + // Actor configured option? + if opt, ok := a.Actor.Options[name]; ok { + return opt } - a.muData.RLock() - v, _ := a.data[key] - a.muData.RUnlock() + // Doodad default option? + if opt, ok := a.Doodad().Options[name]; ok { + return &level.Option{ + Name: opt.Name, + Type: opt.Type, + Value: opt.Default, + } + } - return v + return nil } // LayerCount returns the number of layers in this actor's drawing. diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 1818433..d162847 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -36,6 +36,12 @@ type Canvas struct { Scrollable bool // Cursor keys will scroll the viewport of this canvas. Zoom int // Zoom level on the canvas. + // Toogle for doodad canvases in the Level Editor to show their buttons. + ShowDoodadButtons bool + doodadButtonFrame ui.Widget // lazy init + doodadButtonFrameHovering bool + OnDoodadConfig func(*Actor) + // Custom label to place in the lower-right corner of the canvas. // Used for e.g. the quantity badge on Inventory items. CornerLabel string diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index b17d9cb..6e949ef 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -97,6 +97,11 @@ func (w *Canvas) InstallScripts() error { w.MakeScriptAPI(vm) vm.Set("Self", vm.Self) + // If there is no script attached, do not try and load or call the main() function. + if actor.Doodad().Script == "" { + continue + } + if _, err := vm.Run(actor.Doodad().Script); err != nil { log.Error("Run script for actor %s failed: %s", actor.ID(), err) } diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index 8b0f3d9..a985816 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -478,6 +478,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { H: actor.Canvas.Size().H, } + // Mouse hover? if WP.Inside(box) { actor.Canvas.Configure(ui.Config{ BorderSize: 1, @@ -486,9 +487,23 @@ func (w *Canvas) loopEditable(ev *event.State) error { Background: render.White, // TODO: cuz the border draws a bgcolor }) + // Show doodad buttons. + actor.Canvas.ShowDoodadButtons = true + // Check for a mouse down event to begin dragging this // canvas around. if keybind.LeftClick(ev) { + // Did they click onto the doodad buttons? + if shmem.Cursor.Inside(actor.Canvas.doodadButtonRect()) { + keybind.ClearLeftClick(ev) + if w.OnDoodadConfig != nil { + w.OnDoodadConfig(actor) + } else { + log.Error("OnDoodadConfig: handler not defined for parent canvas") + } + return nil + } + // Pop this canvas out for the drag/drop. if w.OnDragStart != nil { deleteActors = append(deleteActors, actor) @@ -502,6 +517,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { actor.Canvas.SetBorderSize(0) actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO + actor.Canvas.ShowDoodadButtons = false } } diff --git a/pkg/uix/canvas_present.go b/pkg/uix/canvas_present.go index af1f3b0..4ab260e 100644 --- a/pkg/uix/canvas_present.go +++ b/pkg/uix/canvas_present.go @@ -6,6 +6,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/sprites" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" ) @@ -207,6 +208,7 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { w.drawActors(e, p) w.presentStrokes(e) + w.presentDoodadButtons(e) w.presentCursor(e) // Custom label in the canvas corner? (e.g. for Inventory item counts) @@ -270,3 +272,70 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { }) } } + +// Draw doodad buttons on mouseover in the level editor. +func (w *Canvas) presentDoodadButtons(e render.Engine) { + if !w.ShowDoodadButtons || w.parent == nil { + return + } + + // Initialize the buttons the first time? + if w.doodadButtonFrame == nil { + var ( + img ui.Widget + err error + ) + + img, err = sprites.LoadImage(e, balance.GearIcon) + if err != nil { + // Error loading sprite, make a fallback frame. + frame := ui.NewFrame("Buttons") + frame.Configure(ui.Config{ + Width: balance.UICanvasDoodadButtonSize, + Height: balance.UICanvasDoodadButtonSize, + Background: render.Green, + }) + w.doodadButtonFrame = frame + } else { + w.doodadButtonFrame = img + } + + w.doodadButtonFrame.Compute(e) + } + + // log.Error("presentDoodadButtons: parentP=%s w at %s (abs %s) actor at %s draw at %s", parentP, w.Point(), P, actorPoint, drawAt) + w.doodadButtonFrame.Present(e, w.doodadButtonRect().Point()) +} + +// Return the screen rectangle where the doodad buttons would draw. +// screenCords: pass true to get on-screen coords (ignores scroll offset) +func (w *Canvas) doodadButtonRect() render.Rect { + if !w.ShowDoodadButtons || w.parent == nil { + return render.Rect{} + } + + var ( + parentP = ui.AbsolutePosition(w.parent) + scroll = w.parent.Scroll + actorPoint = w.actor.Position() + actorSize = w.Size() + drawAt = render.Point{ + X: parentP.X + scroll.X + actorPoint.X + actorSize.W - balance.UICanvasDoodadButtonSize - w.BoxThickness(1), + Y: parentP.Y + scroll.Y + actorPoint.Y + w.BoxThickness(1), + } + ) + + // If the doodad is Very Small so that its buttons take up a disproportionate + // amount of its space, draw the buttons further to the right. + if actorSize.W <= balance.UICanvasDoodadButtonSpaceNeeded { + drawAt.X += balance.UICanvasDoodadButtonSize / 2 + drawAt.Y -= balance.UICanvasDoodadButtonSize / 2 + } + + return render.Rect{ + X: drawAt.X, + Y: drawAt.Y, + W: drawAt.X + balance.UICanvasDoodadButtonSize, + H: drawAt.Y + balance.UICanvasDoodadButtonSize, + } +} diff --git a/pkg/uix/magic-form/magic_form.go b/pkg/uix/magic-form/magic_form.go index 2569e40..c016a27 100644 --- a/pkg/uix/magic-form/magic_form.go +++ b/pkg/uix/magic-form/magic_form.go @@ -18,6 +18,7 @@ const ( Text // free, wide Label row Frame // custom frame from the caller Button // Single button with a label + Value // a Label & Value row (value not editable) Textbox Checkbox Radiobox @@ -69,6 +70,7 @@ type Field struct { Options []Option // Selectbox SelectValue interface{} // Selectbox default choice Color *render.Color // Color + Readonly bool // draw the value as a flat label // For text-type fields, opt-in to let magicform prompt the // user using the game's developer shell. @@ -189,6 +191,28 @@ func (form Form) Create(into *ui.Frame, fields []Field) { }) } + // Simple "Value" row with a Label to its left. + if row.Type == Value { + lbl := ui.NewLabel(ui.Label{ + Text: row.Label, + Font: row.Font, + TextVariable: row.TextVariable, + IntVariable: row.IntVariable, + }) + + frame.Pack(lbl, ui.Pack{ + Side: ui.W, + FillX: true, + Expand: true, + }) + + // Tooltip? TODO - make nicer. + if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil { + tt := ui.NewTooltip(lbl, row.Tooltip) + tt.Supervise(form.Supervisor) + } + } + // Color picker button. if row.Type == Color && row.Color != nil { btn := ui.NewButton("ColorPicker", ui.NewLabel(ui.Label{ @@ -268,13 +292,18 @@ func (form Form) Create(into *ui.Frame, fields []Field) { TextVariable: row.TextVariable, IntVariable: row.IntVariable, })) - form.Supervisor.Add(btn) + frame.Pack(btn, ui.Pack{ Side: ui.W, FillX: true, Expand: true, }) + // Not clickable if Readonly. + if !row.Readonly { + form.Supervisor.Add(btn) + } + // Tooltip? TODO - make nicer. if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil { tt := ui.NewTooltip(btn, row.Tooltip) diff --git a/pkg/uix/scripting.go b/pkg/uix/scripting.go index 23e04ce..e36c010 100644 --- a/pkg/uix/scripting.go +++ b/pkg/uix/scripting.go @@ -90,7 +90,15 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} { var size = actor.Doodad().ChunkSize() return render.NewRect(size, size) }, - "GetTag": actor.Doodad().Tag, + "GetTag": actor.Doodad().Tag, + "Options": actor.Options, + "GetOption": func(name string) interface{} { + opt := actor.GetOption(name) + if opt == nil { + return nil + } + return opt.Value + }, "Position": actor.Position, "MoveTo": func(p render.Point) { actor.MoveTo(p) diff --git a/pkg/windows/doodad_config.go b/pkg/windows/doodad_config.go new file mode 100644 index 0000000..8313e58 --- /dev/null +++ b/pkg/windows/doodad_config.go @@ -0,0 +1,319 @@ +package windows + +import ( + "fmt" + "sort" + + "git.kirsle.net/SketchyMaze/doodle/pkg/balance" + "git.kirsle.net/SketchyMaze/doodle/pkg/level" + "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" + "git.kirsle.net/SketchyMaze/doodle/pkg/uix" + magicform "git.kirsle.net/SketchyMaze/doodle/pkg/uix/magic-form" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +// DoodadConfig window is what pops up in Edit Mode when you mouse over +// an actor and click its properties button. +type DoodadConfig struct { + // Settings passed in by doodle + Supervisor *ui.Supervisor + Engine render.Engine + + // Configuration options. + EditActor *uix.Actor + ActiveTab string // specify the tab to open + OnRefresh func() // caller should rebuild the window + + // Widgets. + TabFrame *ui.TabFrame +} + +// NewSettingsWindow initializes the window. +func NewDoodadConfigWindow(cfg *DoodadConfig) *ui.Window { + var ( + Width = 400 + Height = 300 + ) + + window := ui.NewWindow(cfg.EditActor.Doodad().Title + " - Actor Properties") + window.SetButtons(ui.CloseButton) + window.Configure(ui.Config{ + Width: Width, + Height: Height, + Background: render.Grey, + }) + + /////////// + // Tab Bar + tabFrame := ui.NewTabFrame("Tab Frame") + tabFrame.SetBackground(render.DarkGrey) + window.Pack(tabFrame, ui.Pack{ + Side: ui.N, + FillX: true, + }) + cfg.TabFrame = tabFrame + + // Make the tabs. + cfg.makeMetaTab(tabFrame, Width, Height) + cfg.makeOptionsTab(tabFrame, Width, Height) + + tabFrame.Supervise(cfg.Supervisor) + + return window +} + +// DoodadConfig Window "Metadata" Tab +func (c *DoodadConfig) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { + tab := tabFrame.AddTab("Metadata", ui.NewLabel(ui.Label{ + Text: "Metadata", + Font: balance.TabFont, + })) + tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) + + if c.EditActor == nil { + return tab + } + + var ( + doodad = c.EditActor.Doodad() + actorID = c.EditActor.ID() + // actorPos = c.EditActor.Position().String() + ) + + form := magicform.Form{ + Supervisor: c.Supervisor, + Engine: c.Engine, + Vertical: true, + LabelWidth: 110, + PadY: 2, + } + fields := []magicform.Field{ + { + Label: "Doodad", + Font: balance.LabelFont, + }, + { + Label: "Title:", + Type: magicform.Value, + Font: balance.UIFont, + TextVariable: &doodad.Title, + }, + { + Label: "Author:", + Type: magicform.Value, + Font: balance.UIFont, + TextVariable: &doodad.Author, + }, + + { + Label: "Actor (Doodad instance in level)", + Font: balance.LabelFont, + }, + { + Label: "Actor ID:", + Type: magicform.Value, + Font: balance.UIFont, + TextVariable: &actorID, + }, + /* TODO: doesn't update dynamically enough + { + Label: "World Position:", + Type: magicform.Value, + Font: balance.UIFont, + TextVariable: actorPos, + }, + */ + } + + form.Create(tab, fields) + + return tab +} + +// SetTextable is a Button or Checkbox widget having a SetText function, +// to support the reset button on the Doodad Options tab. +type SetTextable interface { + SetText(string) error +} + +// DoodadConfig Window "Tags" Tab +func (c DoodadConfig) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { + tab := tabFrame.AddTab("Options", ui.NewLabel(ui.Label{ + Text: "Options", + Font: balance.TabFont, + })) + tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) + + if c.EditActor == nil { + return tab + } + + // Draw a table view of the current tags on this doodad. + var ( + doodad = c.EditActor.Doodad() + headers = []string{"Type", "Name", "Value", "Reset"} + columns = []int{40, 130, 130, 80} // TODO, Width=400 + height = 24 + row = ui.NewFrame("HeaderRow") + ) + tab.Pack(row, ui.Pack{ + Side: ui.N, + FillX: true, + }) + for i, value := range headers { + cell := ui.NewLabel(ui.Label{ + Text: value, + Font: balance.MenuFontBold, + }) + cell.Resize(render.NewRect(columns[i], height)) + row.Pack(cell, ui.Pack{ + Side: ui.W, + }) + } + + // No tags? + if len(doodad.Options) == 0 { + label := ui.NewLabel(ui.Label{ + Text: "There are no options on this doodad.", + Font: balance.MenuFont, + }) + tab.Pack(label, ui.Pack{ + Side: ui.N, + FillX: true, + }) + } else { + // Initialize the Actor Options if nil + if c.EditActor.Actor.Options == nil { + c.EditActor.Actor.Options = map[string]*level.Option{} + } + + // Draw the rows for each tag. + var sortedOpts []string + for name := range doodad.Options { + sortedOpts = append(sortedOpts, name) + } + sort.Strings(sortedOpts) + + for _, optName := range sortedOpts { + var ( + name = optName + value = c.EditActor.GetOption(name) + ) + + if value == nil { + continue + } + + row = ui.NewFrame("Option Row") + tab.Pack(row, ui.Pack{ + Side: ui.N, + FillX: true, + PadY: 2, + }) + + lblType := ui.NewLabel(ui.Label{ + Text: value.Type, + Font: balance.MenuFont, + }) + lblType.Resize(render.NewRect(columns[0], height)) + + lblName := ui.NewLabel(ui.Label{ + Text: name, + Font: balance.MenuFont, + }) + lblName.Resize(render.NewRect(columns[1], height)) + + // Value button: show a checkbox for booleans or a clickable + // button for other types (prompts user for value) + var btnValue ui.Widget + var cbValue bool + if value.Type == "bool" { + cbValue = value.Value.(bool) + checkbox := ui.NewCheckbox("Bool Box", &cbValue, ui.NewLabel(ui.Label{ + Text: fmt.Sprintf("%v", cbValue), + Font: balance.MenuFont, + })) + checkbox.Resize(render.NewRect(columns[2], height)) + checkbox.Handle(ui.Click, func(ed ui.EventData) error { + var label string + if cbValue { + label = "true" + } else { + label = "false" + } + c.EditActor.Actor.SetOption(name, value.Type, label) + checkbox.SetText(label) + return nil + }) + checkbox.Supervise(c.Supervisor) + btnValue = checkbox + } else { + button := ui.NewButton("Tag Button", ui.NewLabel(ui.Label{ + Text: fmt.Sprintf("%v", value.Value), + Font: balance.MenuFont, + })) + button.Resize(render.NewRect(columns[2], height)) + button.Handle(ui.Click, func(ed ui.EventData) error { + shmem.Prompt("Enter new value: ", func(answer string) { + if answer == "" { + return + } + answer = c.EditActor.Actor.SetOption(name, value.Type, answer) + button.SetText(answer) + }) + return nil + }) + c.Supervisor.Add(button) + btnValue = button + } + + // "Delete" / Reset Button: removes the Actor Option so it falls + // back onto the default Doodad Option. + btnDelete := ui.NewButton("Delete Button", ui.NewLabel(ui.Label{ + Text: "Reset", + Font: balance.MenuFont, + })) + btnDelete.Resize(render.NewRect(columns[3], height)) + btnDelete.SetStyle(&balance.ButtonDanger) + btnDelete.Handle(ui.Click, func(ed ui.EventData) error { + log.Info("Delete option: %s", name) + delete(c.EditActor.Actor.Options, name) + + // Update the value button's text label. + if stt, ok := btnValue.(SetTextable); ok { + value := c.EditActor.GetOption(name) + if value != nil { + stt.SetText(fmt.Sprintf("%v", value.Value)) + + // Set the correct boolean checkbox state. + if value.Type == "bool" { + cbValue = value.Value.(bool) + } + } + } + + return nil + }) + c.Supervisor.Add(btnDelete) + + // Pack the widgets. + row.Pack(lblType, ui.Pack{ + Side: ui.W, + }) + row.Pack(lblName, ui.Pack{ + Side: ui.W, + }) + row.Pack(btnValue, ui.Pack{ + Side: ui.W, + PadX: 4, + }) + row.Pack(btnDelete, ui.Pack{ + Side: ui.W, + }) + } + } + + return tab +} diff --git a/pkg/windows/doodad_properties.go b/pkg/windows/doodad_properties.go index b68bda6..316dce4 100644 --- a/pkg/windows/doodad_properties.go +++ b/pkg/windows/doodad_properties.go @@ -79,7 +79,10 @@ type DoodadProperties struct { } // HACKY GLOBAL VARIABLE -var showTagsOnRefreshDoodadPropertiesWindow bool +var ( + showTagsOnRefreshDoodadPropertiesWindow bool + showOptsOnRefreshDoodadPropertiesWindow bool +) // NewSettingsWindow initializes the window. func NewDoodadPropertiesWindow(cfg *DoodadProperties) *ui.Window { @@ -109,10 +112,14 @@ func NewDoodadPropertiesWindow(cfg *DoodadProperties) *ui.Window { // Make the tabs. cfg.makeMetaTab(tabFrame, Width, Height) cfg.makeTagsTab(tabFrame, Width, Height) + cfg.makeOptionsTab(tabFrame, Width, Height) if showTagsOnRefreshDoodadPropertiesWindow { tabFrame.SetTab("Tags") showTagsOnRefreshDoodadPropertiesWindow = false + } else if showOptsOnRefreshDoodadPropertiesWindow { + tabFrame.SetTab("Options") + showOptsOnRefreshDoodadPropertiesWindow = false } tabFrame.Supervise(cfg.Supervisor) @@ -667,6 +674,215 @@ func (c DoodadProperties) makeTagsTab(tabFrame *ui.TabFrame, Width, Height int) return tab } +// DoodadProperties Window "Options" Tab +func (c DoodadProperties) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame { + tab := tabFrame.AddTab("Options", ui.NewLabel(ui.Label{ + Text: "Options", + Font: balance.TabFont, + })) + tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46)) + + if c.EditDoodad == nil { + return tab + } + + // Draw a table view of the current tags on this doodad. + var ( + headers = []string{"Type", "Name", "Default", "Del."} + columns = []int{40, 130, 130, 80} // TODO, Width=400 + height = 24 + row = ui.NewFrame("HeaderRow") + ) + tab.Pack(row, ui.Pack{ + Side: ui.N, + FillX: true, + }) + for i, value := range headers { + cell := ui.NewLabel(ui.Label{ + Text: value, + Font: balance.MenuFontBold, + }) + cell.Resize(render.NewRect(columns[i], height)) + row.Pack(cell, ui.Pack{ + Side: ui.W, + }) + } + + // No tags? + if len(c.EditDoodad.Options) == 0 { + label := ui.NewLabel(ui.Label{ + Text: "There are no options on this doodad.", + Font: balance.MenuFont, + }) + tab.Pack(label, ui.Pack{ + Side: ui.N, + FillX: true, + }) + } else { + // Draw the rows for each tag. + var sortedOpts []string + for name := range c.EditDoodad.Options { + sortedOpts = append(sortedOpts, name) + } + sort.Strings(sortedOpts) + + for _, optName := range sortedOpts { + var ( + name = optName + value = c.EditDoodad.Options[name] + ) + + row = ui.NewFrame("Option Row") + tab.Pack(row, ui.Pack{ + Side: ui.N, + FillX: true, + PadY: 2, + }) + + lblType := ui.NewLabel(ui.Label{ + Text: value.Type, + Font: balance.MenuFont, + }) + lblType.Resize(render.NewRect(columns[0], height)) + + lblName := ui.NewLabel(ui.Label{ + Text: name, + Font: balance.MenuFont, + }) + lblName.Resize(render.NewRect(columns[1], height)) + + // Value button: show a checkbox for booleans or a clickable + // button for other types (prompts user for value) + var btnValue ui.Widget + if value.Type == "bool" { + var cbValue = value.Default.(bool) + checkbox := ui.NewCheckbox("Bool Box", &cbValue, ui.NewLabel(ui.Label{ + Text: fmt.Sprintf("%v", cbValue), + Font: balance.MenuFont, + })) + checkbox.Resize(render.NewRect(columns[2], height)) + checkbox.Handle(ui.Click, func(ed ui.EventData) error { + var label string + if cbValue { + label = "true" + } else { + label = "false" + } + c.EditDoodad.Options[name].Set(label) + checkbox.SetText(label) + return nil + }) + checkbox.Supervise(c.Supervisor) + btnValue = checkbox + } else { + button := ui.NewButton("Tag Button", ui.NewLabel(ui.Label{ + Text: fmt.Sprintf("%v", value.Default), + Font: balance.MenuFont, + })) + button.Resize(render.NewRect(columns[2], height)) + button.Handle(ui.Click, func(ed ui.EventData) error { + shmem.Prompt("Enter new value: ", func(answer string) { + if answer == "" { + return + } + answer = c.EditDoodad.Options[name].Set(answer) + button.SetText(answer) + }) + return nil + }) + c.Supervisor.Add(button) + btnValue = button + } + + btnDelete := ui.NewButton("Delete Button", ui.NewLabel(ui.Label{ + Text: "Del", + Font: balance.MenuFont, + })) + btnDelete.Resize(render.NewRect(columns[3], height)) + btnDelete.SetStyle(&balance.ButtonDanger) + btnDelete.Handle(ui.Click, func(ed ui.EventData) error { + modal.Confirm("Delete option %s?", name).Then(func() { + log.Info("Delete option: %s", name) + delete(c.EditDoodad.Options, name) + + // Trigger a refresh. + if c.OnRefresh != nil { + showOptsOnRefreshDoodadPropertiesWindow = true + c.OnRefresh() + } + }) + return nil + }) + c.Supervisor.Add(btnDelete) + + // Pack the widgets. + row.Pack(lblType, ui.Pack{ + Side: ui.W, + }) + row.Pack(lblName, ui.Pack{ + Side: ui.W, + }) + row.Pack(btnValue, ui.Pack{ + Side: ui.W, + PadX: 4, + }) + row.Pack(btnDelete, ui.Pack{ + Side: ui.W, + }) + } + } + + // Add Option menu button. + row = ui.NewFrame("Button Frame") + tab.Pack(row, ui.Pack{ + Side: ui.N, + FillX: true, + }) + btnAdd := ui.NewMenuButton("New Option", ui.NewLabel(ui.Label{ + Text: "Add Option", + Font: balance.MenuFont, + })) + btnAdd.SetStyle(&balance.ButtonPrimary) + + // Types of options + for _, item := range []struct { + label string + typing string + value interface{} + }{ + {"Boolean", "bool", false}, + {"String", "str", ""}, + {"Integer", "int", 0}, + } { + item := item + btnAdd.AddItem(item.label, func() { + shmem.Prompt("Enter name of the new boolean: ", func(answer string) { + if answer == "" { + return + } + + c.EditDoodad.Options[answer] = &doodads.Option{ + Name: answer, + Type: item.typing, + Default: item.value, + } + if c.OnRefresh != nil { + showOptsOnRefreshDoodadPropertiesWindow = true + c.OnRefresh() + } + }) + }) + } + + btnAdd.Supervise(c.Supervisor) + c.Supervisor.Add(btnAdd) + row.Pack(btnAdd, ui.Pack{ + Side: ui.E, + }) + + return tab +} + func (c DoodadProperties) reloadTagFrame() { }