Noah Petherbridge
48e18da511
* If the player runs the PlayAsBird cheat they shouldn't be able to win a high score on a level, so at level startup it detects whether the DefaultPlayerCharacterDoodad has changed from default on a level that doesn't use the Start Flag to set a specific doodad - and immediately marks the session as cheated
538 lines
12 KiB
Go
538 lines
12 KiB
Go
package windows
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
|
"git.kirsle.net/apps/doodle/pkg/modal"
|
|
"git.kirsle.net/apps/doodle/pkg/savegame"
|
|
"git.kirsle.net/apps/doodle/pkg/sprites"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/ui"
|
|
)
|
|
|
|
// LevelPack window lets the user open and play a level from a pack.
|
|
type LevelPack struct {
|
|
Supervisor *ui.Supervisor
|
|
Engine render.Engine
|
|
|
|
// Callback functions.
|
|
OnPlayLevel func(pack levelpack.LevelPack, level levelpack.Level)
|
|
OnCloseWindow func()
|
|
|
|
// Internal variables
|
|
window *ui.Window
|
|
tabFrame *ui.TabFrame
|
|
savegame *savegame.SaveGame
|
|
goldSprite *ui.Image
|
|
silverSprite *ui.Image
|
|
}
|
|
|
|
// NewLevelPackWindow initializes the window.
|
|
func NewLevelPackWindow(config LevelPack) *ui.Window {
|
|
// Default options.
|
|
var (
|
|
title = "Select a Level"
|
|
|
|
// size of the popup window
|
|
width = 320
|
|
height = 360
|
|
)
|
|
|
|
// Get the available .levelpack files.
|
|
lpFiles, packmap, err := levelpack.LoadAllAvailable()
|
|
if err != nil {
|
|
log.Error("Couldn't list levelpack files: %s", err)
|
|
}
|
|
|
|
// Load the user's savegame.json
|
|
sg, err := savegame.GetOrCreate()
|
|
config.savegame = sg
|
|
if err != nil {
|
|
log.Warn("NewLevelPackWindow: didn't load savegame json (fresh struct created): %s", err)
|
|
}
|
|
|
|
// Cache the gold/silver sprite icons.
|
|
if goldSprite, err := sprites.LoadImage(config.Engine, "sprites/gold.png"); err == nil {
|
|
config.goldSprite = goldSprite
|
|
}
|
|
if silverSprite, err := sprites.LoadImage(config.Engine, "sprites/silver.png"); err == nil {
|
|
config.silverSprite = silverSprite
|
|
}
|
|
|
|
window := ui.NewWindow(title)
|
|
window.SetButtons(ui.CloseButton)
|
|
window.Configure(ui.Config{
|
|
Width: width,
|
|
Height: height,
|
|
Background: render.Grey,
|
|
})
|
|
config.window = window
|
|
|
|
frame := ui.NewFrame("Window Body Frame")
|
|
window.Pack(frame, ui.Pack{
|
|
Side: ui.N,
|
|
Fill: true,
|
|
Expand: true,
|
|
})
|
|
|
|
// Use a TabFrame to organize the "screens" of this window.
|
|
// The default screen is a pager for LevelPacks,
|
|
// And each LevelPack's screen is a pager for its Levels.
|
|
tabFrame := ui.NewTabFrame("Screens Manager")
|
|
tabFrame.SetTabsHidden(true)
|
|
tabFrame.Supervise(config.Supervisor)
|
|
window.Pack(tabFrame, ui.Pack{
|
|
Side: ui.N,
|
|
FillX: true,
|
|
})
|
|
config.tabFrame = tabFrame
|
|
|
|
// Make the tabs.
|
|
indexTab := tabFrame.AddTab("LevelPacks", ui.NewLabel(ui.Label{
|
|
Text: "LevelPacks",
|
|
Font: balance.TabFont,
|
|
}))
|
|
config.makeIndexScreen(indexTab, width, height, lpFiles, packmap, func(screen string) {
|
|
// Callback for user choosing a level pack.
|
|
// Hide the index screen and show the screen for this pack.
|
|
tabFrame.SetTab(screen)
|
|
})
|
|
for _, filename := range lpFiles {
|
|
tab := tabFrame.AddTab(filename, ui.NewLabel(ui.Label{
|
|
Text: filename,
|
|
Font: balance.TabFont,
|
|
}))
|
|
config.makeDetailScreen(tab, width, height, packmap[filename])
|
|
}
|
|
|
|
// Close button.
|
|
if config.OnCloseWindow != nil {
|
|
closeBtn := ui.NewButton("Close Window", ui.NewLabel(ui.Label{
|
|
Text: "Close",
|
|
Font: balance.MenuFont,
|
|
}))
|
|
closeBtn.Handle(ui.Click, func(ed ui.EventData) error {
|
|
config.OnCloseWindow()
|
|
return nil
|
|
})
|
|
config.Supervisor.Add(closeBtn)
|
|
window.Place(closeBtn, ui.Place{
|
|
Bottom: 15,
|
|
Center: true,
|
|
})
|
|
}
|
|
|
|
window.Supervise(config.Supervisor)
|
|
window.Hide()
|
|
return window
|
|
}
|
|
|
|
/* Index screen for the LevelPack window.
|
|
|
|
frame: a TabFrame to populate
|
|
*/
|
|
func (config LevelPack) makeIndexScreen(frame *ui.Frame, width, height int,
|
|
lpFiles []string, packmap map[string]levelpack.LevelPack, onChoose func(string)) {
|
|
var (
|
|
buttonHeight = 60 // height of each LevelPack button
|
|
buttonWidth = width - 40
|
|
|
|
// pagination values
|
|
page = 1
|
|
pages int
|
|
perPage = 3
|
|
maxPageButtons = 10
|
|
)
|
|
|
|
label := ui.NewLabel(ui.Label{
|
|
Text: "Select from a Level Pack below:",
|
|
Font: balance.LabelFont,
|
|
})
|
|
frame.Pack(label, ui.Pack{
|
|
Side: ui.N,
|
|
PadX: 8,
|
|
PadY: 8,
|
|
})
|
|
|
|
pages = int(
|
|
math.Ceil(
|
|
float64(len(lpFiles)) / float64(perPage),
|
|
),
|
|
)
|
|
|
|
var buttons []*ui.Button
|
|
for i, filename := range lpFiles {
|
|
filename := filename
|
|
lp, ok := packmap[filename]
|
|
if !ok {
|
|
log.Error("Couldn't find %s in packmap!", filename)
|
|
continue
|
|
}
|
|
|
|
// Make a frame to hold a complex button layout.
|
|
btnFrame := ui.NewFrame("Frame")
|
|
btnFrame.Resize(render.Rect{
|
|
W: buttonWidth,
|
|
H: buttonHeight,
|
|
})
|
|
|
|
// Draw labels...
|
|
label := ui.NewLabel(ui.Label{
|
|
Text: lp.Title,
|
|
Font: balance.LabelFont,
|
|
})
|
|
btnFrame.Pack(label, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
description := lp.Description
|
|
if description == "" {
|
|
description = "(No description)"
|
|
}
|
|
|
|
byline := ui.NewLabel(ui.Label{
|
|
Text: description,
|
|
Font: balance.MenuFont,
|
|
})
|
|
btnFrame.Pack(byline, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
numLevels := ui.NewLabel(ui.Label{
|
|
Text: fmt.Sprintf("[completed %d of %d levels]", config.savegame.CountCompleted(lp.Filename), len(lp.Levels)),
|
|
Font: balance.MenuFont,
|
|
})
|
|
btnFrame.Pack(numLevels, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
button := ui.NewButton(filename, btnFrame)
|
|
button.Handle(ui.Click, func(ed ui.EventData) error {
|
|
onChoose(filename)
|
|
return nil
|
|
})
|
|
|
|
frame.Pack(button, ui.Pack{
|
|
Side: ui.N,
|
|
PadY: 2,
|
|
})
|
|
config.Supervisor.Add(button)
|
|
|
|
if i > perPage-1 {
|
|
button.Hide()
|
|
}
|
|
buttons = append(buttons, button)
|
|
}
|
|
|
|
pager := ui.NewPager(ui.Pager{
|
|
Name: "LevelPack 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) * perPage
|
|
visible = 0
|
|
)
|
|
for i, row := range buttons {
|
|
if visible >= perPage {
|
|
row.Hide()
|
|
continue
|
|
}
|
|
|
|
if i < minRow {
|
|
row.Hide()
|
|
} else {
|
|
row.Show()
|
|
visible++
|
|
}
|
|
}
|
|
},
|
|
})
|
|
pager.Compute(config.Engine)
|
|
pager.Supervise(config.Supervisor)
|
|
frame.Pack(pager, ui.Pack{
|
|
Side: ui.N,
|
|
PadY: 2,
|
|
})
|
|
}
|
|
|
|
// Detail screen for a given levelpack.
|
|
func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp levelpack.LevelPack) *ui.Frame {
|
|
var (
|
|
buttonHeight = 40
|
|
buttonWidth = width - 40
|
|
|
|
page = 1
|
|
perPage = 4
|
|
pages = int(
|
|
math.Ceil(
|
|
float64(len(lp.Levels)) / float64(perPage),
|
|
),
|
|
)
|
|
maxPageButtons = 10
|
|
)
|
|
|
|
// Load the padlock icon for locked levels.
|
|
// If not loadable, won't be used in UI.
|
|
padlock, _ := sprites.LoadImage(config.Engine, balance.LockIcon)
|
|
|
|
// How many levels completed?
|
|
var (
|
|
numCompleted = config.savegame.CountCompleted(lp.Filename)
|
|
numUnlocked = lp.FreeLevels + numCompleted
|
|
)
|
|
|
|
/** Back Button */
|
|
backButton := ui.NewButton("Back", ui.NewLabel(ui.Label{
|
|
Text: "< Back",
|
|
Font: ui.MenuFont,
|
|
}))
|
|
backButton.SetStyle(&balance.ButtonBabyBlue)
|
|
backButton.Handle(ui.Click, func(ed ui.EventData) error {
|
|
config.tabFrame.SetTab("LevelPacks")
|
|
return nil
|
|
})
|
|
config.Supervisor.Add(backButton)
|
|
frame.Pack(backButton, ui.Pack{
|
|
Side: ui.NE,
|
|
PadY: 2,
|
|
PadX: 6,
|
|
})
|
|
|
|
// Spacer: the back button is position NW and the rest against N
|
|
// so may overlap.
|
|
spacer := ui.NewFrame("Spacer")
|
|
spacer.Configure(ui.Config{
|
|
Width: 64,
|
|
Height: 30,
|
|
})
|
|
frame.Pack(spacer, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
// LevelPack Title label
|
|
label := ui.NewLabel(ui.Label{
|
|
Text: lp.Title,
|
|
Font: balance.LabelFont,
|
|
})
|
|
frame.Pack(label, ui.Pack{
|
|
Side: ui.NW,
|
|
PadX: 8,
|
|
PadY: 2,
|
|
})
|
|
|
|
// Description
|
|
if lp.Description != "" {
|
|
label := ui.NewLabel(ui.Label{
|
|
Text: lp.Description,
|
|
Font: balance.MenuFont,
|
|
})
|
|
frame.Pack(label, ui.Pack{
|
|
Side: ui.N,
|
|
PadX: 8,
|
|
PadY: 2,
|
|
})
|
|
}
|
|
|
|
// Byline
|
|
if lp.Author != "" {
|
|
label := ui.NewLabel(ui.Label{
|
|
Text: "by " + lp.Author,
|
|
Font: balance.MenuFont,
|
|
})
|
|
frame.Pack(label, ui.Pack{
|
|
Side: ui.N,
|
|
PadX: 8,
|
|
PadY: 2,
|
|
})
|
|
}
|
|
|
|
// Loop over all the levels in this pack.
|
|
var buttons []*ui.Button
|
|
for i, level := range lp.Levels {
|
|
level := level
|
|
score := config.savegame.GetLevelScore(lp.Filename, level.Filename)
|
|
|
|
// Make a frame to hold a complex button layout.
|
|
btnFrame := ui.NewFrame("Frame")
|
|
btnFrame.Resize(render.Rect{
|
|
W: buttonWidth,
|
|
H: buttonHeight,
|
|
})
|
|
|
|
// Padlock icon in the corner.
|
|
var locked = lp.FreeLevels > 0 && i+1 > numUnlocked
|
|
if locked && padlock != nil {
|
|
btnFrame.Pack(padlock, ui.Pack{
|
|
Side: ui.NE,
|
|
Padding: 4,
|
|
})
|
|
}
|
|
|
|
// Title Line
|
|
title := ui.NewLabel(ui.Label{
|
|
Text: level.Title,
|
|
Font: balance.LabelFont,
|
|
})
|
|
btnFrame.Pack(title, ui.Pack{
|
|
Side: ui.NW,
|
|
})
|
|
|
|
// Score Frame
|
|
detail := ui.NewFrame("Score")
|
|
btnFrame.Pack(detail, ui.Pack{
|
|
Side: ui.NW,
|
|
})
|
|
if score.Completed {
|
|
check := ui.NewLabel(ui.Label{
|
|
Text: "✓ Completed",
|
|
Font: balance.MenuFont,
|
|
})
|
|
detail.Pack(check, ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
|
|
// Perfect Time
|
|
if score.PerfectTime != nil {
|
|
perfFrame := ui.NewFrame("Perfect Score")
|
|
detail.Pack(perfFrame, ui.Pack{
|
|
Side: ui.W,
|
|
PadX: 8,
|
|
})
|
|
|
|
if config.goldSprite != nil {
|
|
perfFrame.Pack(config.goldSprite, ui.Pack{
|
|
Side: ui.W,
|
|
PadX: 1,
|
|
})
|
|
}
|
|
|
|
timeLabel := ui.NewLabel(ui.Label{
|
|
Text: savegame.FormatDuration(*score.PerfectTime),
|
|
Font: balance.MenuFont.Update(render.Text{
|
|
Color: render.DarkYellow,
|
|
}),
|
|
})
|
|
perfFrame.Pack(timeLabel, ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
}
|
|
|
|
// Best Time (non-perfect)
|
|
if score.BestTime != nil {
|
|
bestFrame := ui.NewFrame("Best Score")
|
|
detail.Pack(bestFrame, ui.Pack{
|
|
Side: ui.W,
|
|
PadX: 4,
|
|
})
|
|
|
|
if config.silverSprite != nil {
|
|
bestFrame.Pack(config.silverSprite, ui.Pack{
|
|
Side: ui.W,
|
|
PadX: 1,
|
|
})
|
|
}
|
|
|
|
timeLabel := ui.NewLabel(ui.Label{
|
|
Text: savegame.FormatDuration(*score.BestTime),
|
|
Font: balance.MenuFont.Update(render.Text{
|
|
Color: render.DarkGreen,
|
|
}),
|
|
})
|
|
bestFrame.Pack(timeLabel, ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
}
|
|
} else {
|
|
detail.Pack(ui.NewLabel(ui.Label{
|
|
Text: "Not completed",
|
|
Font: balance.MenuFont,
|
|
}), ui.Pack{
|
|
Side: ui.W,
|
|
})
|
|
}
|
|
|
|
btn := ui.NewButton(level.Filename, btnFrame)
|
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
|
// Is this level locked?
|
|
if locked {
|
|
modal.Alert(
|
|
"This level hasn't been unlocked! Complete the earlier\n" +
|
|
"levels in this pack to unlock later levels.",
|
|
).WithTitle("Locked Level")
|
|
return nil
|
|
}
|
|
|
|
// Play Level
|
|
if config.OnPlayLevel != nil {
|
|
config.OnPlayLevel(lp, level)
|
|
} else {
|
|
log.Error("LevelPack Window: OnPlayLevel callback not ready")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
frame.Pack(btn, ui.Pack{
|
|
Side: ui.N,
|
|
PadY: 2,
|
|
})
|
|
config.Supervisor.Add(btn)
|
|
|
|
if i > perPage-1 {
|
|
btn.Hide()
|
|
}
|
|
buttons = append(buttons, btn)
|
|
}
|
|
|
|
pager := ui.NewPager(ui.Pager{
|
|
Name: "Level 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) * perPage
|
|
visible = 0
|
|
)
|
|
for i, row := range buttons {
|
|
if visible >= perPage {
|
|
row.Hide()
|
|
continue
|
|
}
|
|
|
|
if i < minRow {
|
|
row.Hide()
|
|
} else {
|
|
row.Show()
|
|
visible++
|
|
}
|
|
}
|
|
},
|
|
})
|
|
pager.Compute(config.Engine)
|
|
pager.Supervise(config.Supervisor)
|
|
frame.Pack(pager, ui.Pack{
|
|
Side: ui.N,
|
|
PadY: 2,
|
|
})
|
|
|
|
return frame
|
|
}
|