doodle/pkg/windows/levelpack_open.go
Noah Petherbridge af6b8625d6 Flood Tool, Survival Mode for Azulian Tag
New features:
* Flood Tool for the editor. It replaces pixels of one color with another,
  contiguously. Has limits on how far from the original pixel it will color,
  to avoid infinite loops in case the user clicked on wide open void. The
  limit when clicking an existing color is 1200px or only a 600px limit if
  clicking into the void.
* Cheat code: 'master key' to play locked Story Mode levels.

Level GameRules feature added:
* A new tab in the Level Properties dialog
* Difficulty has been moved to this tab
* Survival Mode: for silver high score, longest time alive is better than
  fastest time, for Azulian Tag maps. Gold high score is still based on
  fastest time - find the hidden level exit without dying!

Tweaks to the Azulians' jump heights:
* Blue Azulian:  12 -> 14
* Red Azulian:   14 -> 18
* White Azulian: 16 -> 20

Bugs fixed:
* When editing your Palette to rename a color or add a new color, it wasn't
  possible to draw with that color until the editor was completely unloaded
  and reloaded; this is now fixed.
* Minor bugfix in Difficulty.String() for Peaceful (-1) difficulty to avoid
  a negative array index.
* Try and prevent user giving the same name to multiple swatches on their
  palette. Replacing the whole palette can let duplication through still.
2022-03-26 13:55:06 -07:00

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 && !balance.CheatEnabledUnlockLevels {
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
}