Noah Petherbridge
97e179716c
New category for the Doodad Dropper: "Technical" Technical doodads have a dashed outline and label for now, and they turn invisible on level start, and are for hidden technical effects on your level. The doodads include: * Goal Region: acts like an invisible Exit Flag (128x128), the level is won when the player character touches this region. * Fire Region: acts like a death barrier (128x128), kills the player when a generic "You have died!" message. * Power Source: on level start, acts like a switch and emits a power(true) signal to all linked doodads. Link it to your Electric Door for it to be open by default in your level! * Stall Player (250ms): The player is paused for a moment the first time it touches this region. Useful to work around timing issues, e.g. help prevent the player from winning a race against another character. There are some UI improvements to the Doodad Dropper window: * If the first page of doodads is short, extra spacers are added so the alignment and size shows correctly. * Added a 'background pattern' to the window: any unoccupied icon space has an inset rectangle slot. * "Last pages" which are short still render weirdly without reserving the correct height in the TabFrame. Doodad scripting engine updates: * Self.Hide() and Self.Show() available. * Subscribe to "broadcast:ready" to know when the level is ready, so you can safely Publish messages without deadlocks!
403 lines
9.6 KiB
Go
403 lines
9.6 KiB
Go
package windows
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"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/log"
|
|
"git.kirsle.net/apps/doodle/pkg/uix"
|
|
"git.kirsle.net/apps/doodle/pkg/usercfg"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/ui"
|
|
)
|
|
|
|
// DoodadDropper is the doodad palette pop-up window for Editor Mode.
|
|
type DoodadDropper struct {
|
|
Supervisor *ui.Supervisor
|
|
Engine render.Engine
|
|
|
|
// Editing settings for an existing level?
|
|
EditLevel *level.Level
|
|
|
|
// Callback functions.
|
|
OnStartDragActor func(doodad *doodads.Doodad, actor *level.Actor)
|
|
OnCancel func()
|
|
}
|
|
|
|
// NewDoodadDropper initializes the window.
|
|
func NewDoodadDropper(config DoodadDropper) *ui.Window {
|
|
// Default options.
|
|
var (
|
|
title = "Doodads"
|
|
|
|
buttonSize = balance.DoodadButtonSize
|
|
columns = balance.DoodadDropperCols
|
|
rows = balance.DoodadDropperRows
|
|
|
|
// size of the doodad window
|
|
width = buttonSize * columns
|
|
height = (buttonSize * rows) + 64 // account for button borders :(
|
|
)
|
|
|
|
// Get all the doodads.
|
|
doodadsAvailable, err := doodads.ListDoodads()
|
|
if err != nil {
|
|
log.Error("NewDoodadDropper: doodads.ListDoodads: %s", err)
|
|
}
|
|
|
|
// Load all the doodads, skip hidden ones.
|
|
var items []*doodads.Doodad
|
|
for _, filename := range doodadsAvailable {
|
|
doodad, err := doodads.LoadFile(filename)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
doodad = doodads.New(balance.DoodadSize)
|
|
}
|
|
|
|
// Skip hidden doodads.
|
|
if doodad.Hidden && !usercfg.Current.ShowHiddenDoodads {
|
|
continue
|
|
}
|
|
|
|
doodad.Filename = filename
|
|
items = append(items, doodad)
|
|
}
|
|
|
|
window := ui.NewWindow(title)
|
|
window.SetButtons(ui.CloseButton)
|
|
window.Configure(ui.Config{
|
|
Width: width,
|
|
Height: height + 30,
|
|
Background: render.Grey,
|
|
})
|
|
|
|
tabFrame := ui.NewTabFrame("Category Tabs")
|
|
window.Pack(tabFrame, ui.Pack{
|
|
Side: ui.N,
|
|
Fill: true,
|
|
Expand: true,
|
|
})
|
|
|
|
// The Category Tabs.
|
|
categories := []struct {
|
|
ID string
|
|
Name string
|
|
}{
|
|
{"objects", "Objects"},
|
|
{"doors", "Doors"},
|
|
{"gizmos", "Gizmos"},
|
|
{"creatures", "Creatures"},
|
|
{"technical", "Technical"},
|
|
{"", "All"},
|
|
}
|
|
for _, category := range categories {
|
|
tab1 := tabFrame.AddTab(category.Name, ui.NewLabel(ui.Label{
|
|
Text: category.Name,
|
|
Font: balance.TabFont,
|
|
}))
|
|
makeDoodadTab(config, tab1, render.NewRect(width-4, height-60), category.ID, items)
|
|
}
|
|
|
|
tabFrame.Supervise(config.Supervisor)
|
|
|
|
window.Hide()
|
|
return window
|
|
}
|
|
|
|
// Function to generate the TabFrame frame of the Doodads window.
|
|
func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, category string, available []*doodads.Doodad) {
|
|
var (
|
|
buttonSize = balance.DoodadButtonSize
|
|
columns = balance.DoodadDropperCols
|
|
rows = balance.DoodadDropperRows
|
|
|
|
// Count how many doodad buttons we need vs. how many can fit.
|
|
iconsDrawn int
|
|
iconsPossible = columns * rows
|
|
|
|
// pagination values
|
|
page = 1
|
|
pages int
|
|
perPage = 20
|
|
maxPageButtons = 10
|
|
)
|
|
frame.Resize(size)
|
|
|
|
// Trim the available doodads to those fitting the category.
|
|
var items = []*doodads.Doodad{}
|
|
for _, candidate := range available {
|
|
if value, ok := candidate.Tags["category"]; ok {
|
|
if category != "" && !strings.Contains(value, category) {
|
|
continue
|
|
}
|
|
} else if category != "" {
|
|
continue
|
|
}
|
|
items = append(items, candidate)
|
|
}
|
|
|
|
doodads.SortByName(items)
|
|
|
|
// Compute the number of pages for the pager widget.
|
|
pages = int(
|
|
math.Ceil(
|
|
float64(len(items)) / float64(columns*rows),
|
|
),
|
|
)
|
|
|
|
// First, draw the empty grid of inset frames to serve as the 'background'
|
|
// of the drawer. This both serves an aesthetic purpose and reserves space
|
|
// in the widget for short page views.
|
|
{
|
|
var (
|
|
decorFrame = ui.NewFrame("Background Slots")
|
|
row *ui.Frame
|
|
)
|
|
|
|
for i := 0; i < iconsPossible; i++ {
|
|
if row == nil || i%columns == 0 {
|
|
row = ui.NewFrame("BG Row")
|
|
decorFrame.Pack(row, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
}
|
|
|
|
spacer := ui.NewFrame("Spacer")
|
|
spacer.Configure(ui.Config{
|
|
BorderSize: 2,
|
|
BorderStyle: ui.BorderSunken,
|
|
Background: render.Grey.Darken(20),
|
|
})
|
|
spacer.Resize(render.NewRect(
|
|
buttonSize-2, // TODO: without the -2 the button border
|
|
buttonSize-2, // rests on top of the window border
|
|
))
|
|
spacer.Compute(config.Engine)
|
|
row.Pack(spacer, ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
}
|
|
|
|
decorFrame.Compute(config.Engine)
|
|
|
|
// frame.Pack(decorFrame, ui.Pack{
|
|
// Side: ui.NW,
|
|
// })
|
|
frame.Place(decorFrame, ui.Place{
|
|
Top: 0,
|
|
Left: 0,
|
|
})
|
|
}
|
|
|
|
// Draw the doodad buttons in rows.
|
|
var btnRows = []*ui.Frame{}
|
|
{
|
|
var (
|
|
row *ui.Frame
|
|
rowCount int // for labeling the ui.Frame for each row
|
|
|
|
// the state we end up at when we exhaust all doodads
|
|
lastColumn int // last position in current row
|
|
)
|
|
|
|
for i, doodad := range items {
|
|
doodad := doodad
|
|
|
|
if row == nil || i%columns == 0 {
|
|
var hidden = rowCount >= rows
|
|
rowCount++
|
|
|
|
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
|
|
|
|
row.Resize(render.NewRect(size.W, buttonSize))
|
|
row.Compute(config.Engine)
|
|
btnRows = append(btnRows, row)
|
|
frame.Pack(row, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
// Hide overflowing rows until we page to them.
|
|
if hidden {
|
|
row.Hide()
|
|
}
|
|
|
|
// New row, new columns.
|
|
lastColumn = 0
|
|
}
|
|
|
|
can := uix.NewCanvas(int(buttonSize), true)
|
|
can.Name = doodad.Title
|
|
can.SetBackground(balance.DoodadButtonBackground)
|
|
can.LoadDoodad(doodad)
|
|
|
|
btn := ui.NewButton(doodad.Title, can)
|
|
btn.Resize(render.NewRect(
|
|
buttonSize-2, // TODO: without the -2 the button border
|
|
buttonSize-2, // rests on top of the window border
|
|
))
|
|
row.Pack(btn, ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
|
|
// Tooltip hover to show the doodad's name.
|
|
ui.NewTooltip(btn, ui.Tooltip{
|
|
Text: doodad.Title,
|
|
Edge: ui.Top,
|
|
})
|
|
|
|
// Begin the drag event to grab this Doodad.
|
|
// NOTE: The drag target is the EditorUI.Canvas in
|
|
// editor_ui.go#SetupCanvas()
|
|
btn.Handle(ui.MouseDown, func(ed ui.EventData) error {
|
|
log.Warn("MouseDown on doodad %s (%s)", doodad.Filename, doodad.Title)
|
|
config.OnStartDragActor(doodad, nil)
|
|
return nil
|
|
})
|
|
config.Supervisor.Add(btn)
|
|
|
|
// Resize the canvas to fill the button interior.
|
|
btnSize := btn.Size()
|
|
can.Resize(render.NewRect(
|
|
btnSize.W-btn.BoxThickness(2),
|
|
btnSize.H-btn.BoxThickness(2),
|
|
))
|
|
|
|
btn.Compute(config.Engine)
|
|
|
|
iconsDrawn++
|
|
lastColumn++
|
|
}
|
|
|
|
// If we have fewer doodad icons than this page can hold,
|
|
// fill out dummy placeholder cells to maintain the UI shape.
|
|
// TODO: this is very redundant compared to the ATTEMPT above
|
|
// to only do this once. It seems our background widget doesn't
|
|
// size up the full tab height properly, so doodad tabs that
|
|
// have fewer than one page worth (short first page) the sizing
|
|
// was wrong. The below hack pads out the screen for short first
|
|
// pages only. There is still a bug with short LAST pages where
|
|
// it doesn't hold height and the pager buttons come up.
|
|
if iconsDrawn < iconsPossible {
|
|
for i := lastColumn; i < iconsPossible; i++ {
|
|
if row == nil || i%columns == 0 {
|
|
var hidden = rowCount >= rows
|
|
rowCount++
|
|
|
|
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
|
|
row.SetBackground(balance.DoodadButtonBackground)
|
|
btnRows = append(btnRows, row)
|
|
frame.Pack(row, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
// Hide overflowing rows until we page to them.
|
|
if hidden {
|
|
row.Hide()
|
|
}
|
|
}
|
|
|
|
spacer := ui.NewFrame("Spacer")
|
|
spacer.Configure(ui.Config{
|
|
BorderSize: 2,
|
|
BorderStyle: ui.BorderSunken,
|
|
Background: render.Grey,
|
|
})
|
|
spacer.Resize(render.NewRect(
|
|
buttonSize-2, // TODO: without the -2 the button border
|
|
buttonSize-2, // rests on top of the window border
|
|
))
|
|
spacer.Compute(config.Engine)
|
|
row.Pack(spacer, ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
|
|
// debug
|
|
// lbl := ui.NewLabel(ui.Label{
|
|
// Text: fmt.Sprintf("i=%d\nrow=%d", i, rowCount),
|
|
// })
|
|
// spacer.Pack(lbl, ui.Pack{
|
|
// Side: ui.NW,
|
|
// })
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
/******************
|
|
* Confirm/cancel buttons.
|
|
******************/
|
|
|
|
bottomFrame := ui.NewFrame("Button Frame")
|
|
frame.Pack(bottomFrame, ui.Pack{
|
|
Side: ui.N,
|
|
FillX: true,
|
|
})
|
|
|
|
// Pager for the doodads.
|
|
pager := ui.NewPager(ui.Pager{
|
|
Name: "Doodad Dropper Pager",
|
|
Page: page,
|
|
Pages: pages,
|
|
PerPage: perPage,
|
|
MaxPageButtons: maxPageButtons,
|
|
Font: balance.MenuFont,
|
|
OnChange: func(newPage, perPage int) {
|
|
page = newPage
|
|
log.Info("Page: %d, %d", page, perPage)
|
|
|
|
// Re-evaluate which rows are shown/hidden for the page we're on.
|
|
var (
|
|
minRow = (page - 1) * rows
|
|
visible = 0
|
|
)
|
|
for i, row := range btnRows {
|
|
if visible >= rows {
|
|
row.Hide()
|
|
continue
|
|
}
|
|
|
|
if i < minRow {
|
|
row.Hide()
|
|
} else {
|
|
row.Show()
|
|
visible++
|
|
}
|
|
}
|
|
},
|
|
})
|
|
pager.Compute(config.Engine)
|
|
pager.Supervise(config.Supervisor)
|
|
bottomFrame.Place(pager, ui.Place{
|
|
Top: 20,
|
|
Left: 20,
|
|
})
|
|
|
|
var buttons = []struct {
|
|
Label string
|
|
F func(ui.EventData) error
|
|
}{
|
|
// OK button is for editing an existing level.
|
|
{"Close", func(ed ui.EventData) error {
|
|
config.OnCancel()
|
|
return nil
|
|
}},
|
|
}
|
|
for _, t := range buttons {
|
|
btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{
|
|
Text: t.Label,
|
|
Font: balance.MenuFont,
|
|
}))
|
|
btn.Handle(ui.Click, t.F)
|
|
config.Supervisor.Add(btn)
|
|
bottomFrame.Place(btn, ui.Place{
|
|
Top: 20,
|
|
Right: 20,
|
|
})
|
|
}
|
|
}
|
|
}
|