Level Thumbnails on Story Mode Select

* Rework the Story Mode UI to display level thumbnails.
  * Responsive UI: defaults to wide screen mode and shows 3 levels horizontally
    but on narrow/mobile display, shows 2 levels per page in portrait.
  * Add "Tiny" screenshot size (224x126) to fit the Story Mode UI.
  * Make the pager buttons bigger and more touchable.
* Maximize the game window on startup unless the -w option with a specific
  window resolution is provided.
This commit is contained in:
Noah 2023-12-09 14:59:31 -08:00
parent 9cce93f431
commit 1a9706c09f
24 changed files with 245 additions and 94 deletions

View File

@ -3,7 +3,6 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"math/rand"
"os" "os"
"regexp" "regexp"
"runtime" "runtime"
@ -47,9 +46,6 @@ func init() {
// Use all the CPU cores for collision detection and other load balanced // Use all the CPU cores for collision detection and other load balanced
// goroutine work in the app. // goroutine work in the app.
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())
// Seed the random number generator.
rand.Seed(time.Now().UnixNano())
} }
func main() { func main() {
@ -154,10 +150,12 @@ func main() {
} }
// Setting a custom resolution? // Setting a custom resolution?
var maximize = true
if c.String("window") != "" { if c.String("window") != "" {
if err := setResolution(c.String("window")); err != nil { if err := setResolution(c.String("window")); err != nil {
panic(err) panic(err)
} }
maximize = false
} }
// Enable feature flags? // Enable feature flags?
@ -200,6 +198,12 @@ func main() {
game := doodle.New(c.Bool("debug"), engine) game := doodle.New(c.Bool("debug"), engine)
game.SetupEngine() game.SetupEngine()
// Start with maximized window unless -w was given.
if maximize {
log.Info("Maximize window")
engine.Maximize()
}
// Reload usercfg - if their settings.json doesn't exist, we try and pick a // Reload usercfg - if their settings.json doesn't exist, we try and pick a
// default "hide touch hints" based on touch device presence - which is only // default "hide touch hints" based on touch device presence - which is only
// known after SetupEngine. // known after SetupEngine.

View File

@ -1,3 +1,4 @@
//go:build shareware
// +build shareware // +build shareware
package balance package balance

View File

@ -1,3 +1,4 @@
//go:build !shareware
// +build !shareware // +build !shareware
package balance package balance

View File

@ -21,10 +21,6 @@ var (
Width = 1024 Width = 1024
Height = 768 Height = 768
// Title screen height needed for the main menu. Phones in landscape
// mode will switch to the horizontal layout if less than this height.
TitleScreenResponsiveHeight = 600
// Speed to scroll a canvas with arrow keys in Edit Mode. // Speed to scroll a canvas with arrow keys in Edit Mode.
CanvasScrollSpeed = 8 CanvasScrollSpeed = 8
FollowActorMaxScrollSpeed = 64 FollowActorMaxScrollSpeed = 64
@ -158,9 +154,11 @@ var (
LevelScreenshotLargeFilename = "large.png" LevelScreenshotLargeFilename = "large.png"
LevelScreenshotMediumFilename = "medium.png" LevelScreenshotMediumFilename = "medium.png"
LevelScreenshotSmallFilename = "small.png" LevelScreenshotSmallFilename = "small.png"
LevelScreenshotTinyFilename = "tiny.png"
LevelScreenshotLargeSize = render.NewRect(1280, 720) LevelScreenshotLargeSize = render.NewRect(1280, 720)
LevelScreenshotMediumSize = render.NewRect(640, 360) LevelScreenshotMediumSize = render.NewRect(640, 360)
LevelScreenshotSmallSize = render.NewRect(320, 180) LevelScreenshotSmallSize = render.NewRect(320, 180) // Level Properties thumbnail size
LevelScreenshotTinySize = render.NewRect(224, 126) // Story Mode thumbnail size
) )
// Edit Mode Values // Edit Mode Values

36
pkg/balance/responsive.go Normal file
View File

@ -0,0 +1,36 @@
package balance
/*
Responsive breakpoints and dimensions for Sketchy Maze.
Ideas for breakpoints (copying web CSS frameworks):
- Mobile up to 768px
- Tablet from 769px
- Desktop from 1024px
- Widescreen from 1216px
- FullHD from 1408px
*/
const (
// Title screen height needed for the main menu. Phones in landscape
// mode will switch to the horizontal layout if less than this height.
TitleScreenResponsiveHeight = 600
BreakpointMobile = 0 // 0-768
BreakpointTablet = 769 // from 769
BreakpointDesktop = 1024 // from 1024
BreakpointWidescreen = 1216
BreakpointFullHD = 1408
)
// IsShortWide is a custom responsive breakpoint to mimic the mobile app in landscape mode like on a Pinephone.
//
// Parameters are the width and height of the application window (usually the screen if maximized).
//
// It is used on the MainScene to decide whether the main menu is drawn tall or wide.
func IsShortWide(width, height int) bool {
return height < TitleScreenResponsiveHeight
}
func IsBreakpointTablet(width, height int) bool {
return width >= BreakpointTablet
}

View File

@ -110,6 +110,7 @@ var (
MenuFont = render.Text{ MenuFont = render.Text{
Size: 12, Size: 12,
PadX: 4, PadX: 4,
PadY: 2,
} }
MenuFontBold = render.Text{ MenuFontBold = render.Text{
FontFilename: SansBoldFont, FontFilename: SansBoldFont,
@ -124,6 +125,14 @@ var (
PadY: 4, PadY: 4,
} }
// Pager styles.
PagerLargeFont = render.Text{
FontFilename: SansBoldFont,
Size: 14,
PadX: 6,
PadY: 4,
}
// Modal backdrop color. // Modal backdrop color.
ModalBackdrop = render.RGBA(1, 1, 1, 42) ModalBackdrop = render.RGBA(1, 1, 1, 42)

View File

@ -21,9 +21,9 @@ EditFile opens a drawing file (Level or Doodad) in the EditorScene.
The filename can be one of the following: The filename can be one of the following:
- A simple filename with no path separators in it and/or no file extension. - A simple filename with no path separators in it and/or no file extension.
- An absolute path beginning with "/" - An absolute path beginning with "/"
- A relative path beginning with "./" - A relative path beginning with "./"
If the filename has an extension (`.level` or `.doodad`), that will disambiguate If the filename has an extension (`.level` or `.doodad`), that will disambiguate
how to find the file and which mode to start the EditorMode in. Otherwise, the how to find the file and which mode to start the EditorMode in. Otherwise, the

View File

@ -7,12 +7,12 @@ Stroke holds temporary pixel data with a shape and color.
It is used for myriad purposes: It is used for myriad purposes:
- As a staging area for drawing new pixels to the drawing without committing - As a staging area for drawing new pixels to the drawing without committing
them until completed. them until completed.
- As a unit of work for the Undo/Redo History when editing a drawing. - As a unit of work for the Undo/Redo History when editing a drawing.
- As imaginary visual lines superimposed on top of a drawing, for example to - As imaginary visual lines superimposed on top of a drawing, for example to
visualize the link between two doodads or to draw collision hitboxes and other visualize the link between two doodads or to draw collision hitboxes and other
debug lines to the drawing. debug lines to the drawing.
*/ */
type Stroke struct { type Stroke struct {
ID int // Unique ID per each stroke ID int // Unique ID per each stroke

View File

@ -133,7 +133,7 @@ func SaveCroppedScreenshot(level *level.Level, viewport render.Rect) (string, er
// UpdateLevelScreenshots will generate and embed the screenshot PNGs into the level data. // UpdateLevelScreenshots will generate and embed the screenshot PNGs into the level data.
func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error { func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error {
// Take screenshots. // Take screenshots.
large, medium, small, err := CreateLevelScreenshots(lvl, scroll) large, medium, small, tiny, err := CreateLevelScreenshots(lvl, scroll)
if err != nil { if err != nil {
return err return err
} }
@ -143,6 +143,7 @@ func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error {
balance.LevelScreenshotLargeFilename: large, balance.LevelScreenshotLargeFilename: large,
balance.LevelScreenshotMediumFilename: medium, balance.LevelScreenshotMediumFilename: medium,
balance.LevelScreenshotSmallFilename: small, balance.LevelScreenshotSmallFilename: small,
balance.LevelScreenshotTinyFilename: tiny,
} { } {
var fh = bytes.NewBuffer([]byte{}) var fh = bytes.NewBuffer([]byte{})
if err := png.Encode(fh, img); err != nil { if err := png.Encode(fh, img); err != nil {
@ -165,7 +166,7 @@ func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error {
// will be embedded within the level data itself. // will be embedded within the level data itself.
// //
// Returns the large, medium and small images. // Returns the large, medium and small images.
func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, medium, small image.Image, err error) { func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, medium, small, tiny image.Image, err error) {
// Viewport to screenshot. // Viewport to screenshot.
viewport := render.Rect{ viewport := render.Rect{
X: scroll.X, X: scroll.X,
@ -183,7 +184,8 @@ func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, mediu
// Scale the medium and small versions. // Scale the medium and small versions.
medium = Scale(large, image.Rect(0, 0, balance.LevelScreenshotMediumSize.W, balance.LevelScreenshotMediumSize.H), draw.ApproxBiLinear) medium = Scale(large, image.Rect(0, 0, balance.LevelScreenshotMediumSize.W, balance.LevelScreenshotMediumSize.H), draw.ApproxBiLinear)
small = Scale(large, image.Rect(0, 0, balance.LevelScreenshotSmallSize.W, balance.LevelScreenshotSmallSize.H), draw.ApproxBiLinear) small = Scale(large, image.Rect(0, 0, balance.LevelScreenshotSmallSize.W, balance.LevelScreenshotSmallSize.H), draw.ApproxBiLinear)
return large, medium, small, nil tiny = Scale(large, image.Rect(0, 0, balance.LevelScreenshotTinySize.W, balance.LevelScreenshotTinySize.H), draw.ApproxBiLinear)
return large, medium, small, tiny, nil
} }
// Scale down an image. Example: // Scale down an image. Example:

View File

@ -9,10 +9,10 @@ by its index number.
This function calls the following: This function calls the following:
* Chunker.Inflate(Palette) to update references to the level's pixels to point - Chunker.Inflate(Palette) to update references to the level's pixels to point
to the Swatch entry. to the Swatch entry.
* Actors.Inflate() - Actors.Inflate()
* Palette.Inflate() to load private instance values for the palette subsystem. - Palette.Inflate() to load private instance values for the palette subsystem.
*/ */
func (l *Level) Inflate() { func (l *Level) Inflate() {
// Inflate the chunk metadata to map the pixels to their palette indexes. // Inflate the chunk metadata to map the pixels to their palette indexes.

View File

@ -497,12 +497,11 @@ func (s *MainScene) Resized(width, height int) {
log.Info("Resized to %dx%d", width, height) log.Info("Resized to %dx%d", width, height)
// If the height is not tall enough for the menu, switch to the horizontal layout. // If the height is not tall enough for the menu, switch to the horizontal layout.
if height < balance.TitleScreenResponsiveHeight { isLandscape := balance.IsShortWide(width, height)
log.Error("Switch to landscape mode") if isLandscape != s.landscapeMode {
s.landscapeMode = true log.Info("Toggled LandscapeMode to: %+v", isLandscape)
} else {
s.landscapeMode = false
} }
s.landscapeMode = isLandscape
s.canvas.Resize(render.Rect{ s.canvas.Resize(render.Rect{
W: width, W: width,

View File

@ -1,3 +1,4 @@
//go:build !js
// +build !js // +build !js
package native package native
@ -14,8 +15,8 @@ import (
// OpenURL opens a web browser to the given URL. // OpenURL opens a web browser to the given URL.
// //
// On Linux this will look for xdg-open or try a few common browser names. // On Linux this will look for xdg-open or try a few common browser names.
// On Windows this uses the ``start`` command. // On Windows this uses the “start“ command.
// On MacOS this uses the ``open`` command. // On MacOS this uses the “open“ command.
func OpenURL(url string) { func OpenURL(url string) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
go windowsOpenURL(url) go windowsOpenURL(url)

View File

@ -1,3 +1,4 @@
//go:build js && wasm
// +build js,wasm // +build js,wasm
package native package native

View File

@ -109,3 +109,10 @@ func TextToImage(e render.Engine, text render.Text) (image.Image, error) {
return img, nil return img, nil
} }
// Set the window to maximized.
func MaximizeWindow(e render.Engine) {
if sdl, ok := e.(*sdl.Renderer); ok {
sdl.Maximize()
}
}

View File

@ -25,3 +25,5 @@ func CopyToClipboard(text string) error {
func CountTextures(e render.Engine) string { func CountTextures(e render.Engine) string {
return "n/a" return "n/a"
} }
func MaximizeWindow(e render.Engine) {}

View File

@ -1,3 +1,4 @@
//go:build js && wasm
// +build js,wasm // +build js,wasm
package native package native
@ -17,4 +18,3 @@ func OpenFile(title string, filter string) (string, error) {
}) })
return "", nil return "", nil
} }

View File

@ -1,3 +1,4 @@
//go:build !js
// +build !js // +build !js
package native package native

View File

@ -10,4 +10,4 @@ var (
// If false, a fallback uses the developer shell Prompt() // If false, a fallback uses the developer shell Prompt()
// to ask for a file name. // to ask for a file name.
FileDialogsReady bool FileDialogsReady bool
) )

View File

@ -7,8 +7,10 @@ RegisterEventHooks attaches the supervisor level event hooks into a JS VM.
Names registered: Names registered:
- EndLevel(): for a doodad to exit the level. Panics if the OnLevelExit - EndLevel(): for a doodad to exit the level. Panics if the OnLevelExit
handler isn't defined. handler isn't defined.
- FailLevel(): for a doodad to cause a level failure.
- SetCheckpoint(): update the player's respawn location.
*/ */
func RegisterEventHooks(s *Supervisor, vm *VM) { func RegisterEventHooks(s *Supervisor, vm *VM) {
vm.Set("EndLevel", func() { vm.Set("EndLevel", func() {

View File

@ -1,4 +1,5 @@
//+build js,wasm //go:build js && wasm
// +build js,wasm
package sound package sound

View File

@ -1,9 +1,9 @@
package wallpaper package wallpaper
import ( import (
"os"
"io/ioutil"
"encoding/base64" "encoding/base64"
"io/ioutil"
"os"
) )
/* /*
@ -30,4 +30,4 @@ func FileToB64(filename string) (string, error) {
} }
return b64, nil return b64, nil
} }

View File

@ -1,3 +1,4 @@
//go:build !js
// +build !js // +build !js
package wasm package wasm

View File

@ -1,3 +1,4 @@
//go:build js && wasm
// +build js,wasm // +build js,wasm
package wasm package wasm

View File

@ -24,11 +24,16 @@ type LevelPack struct {
OnCloseWindow func() OnCloseWindow func()
// Internal variables // Internal variables
isLandscape bool // wide window rather than tall
window *ui.Window window *ui.Window
tabFrame *ui.TabFrame tabFrame *ui.TabFrame
savegame *savegame.SaveGame savegame *savegame.SaveGame
goldSprite *ui.Image goldSprite *ui.Image
silverSprite *ui.Image silverSprite *ui.Image
// Button frames for the footer: one with Back+Close, other with Close only.
footerWithBackButton *ui.Frame
footerWithCloseButton *ui.Frame
} }
// NewLevelPackWindow initializes the window. // NewLevelPackWindow initializes the window.
@ -37,11 +42,18 @@ func NewLevelPackWindow(config LevelPack) *ui.Window {
var ( var (
title = "Select a Level" title = "Select a Level"
// size of the popup window // size of the popup window (vertical)
width = 320 width = 320
height = 360 height = 540
) )
// Are we horizontal?
if balance.IsBreakpointTablet(config.Engine.WindowSize()) {
width = 720
height = 360
config.isLandscape = true
}
// Get the available .levelpack files. // Get the available .levelpack files.
lpFiles, packmap, err := levelpack.LoadAllAvailable() lpFiles, packmap, err := levelpack.LoadAllAvailable()
if err != nil { if err != nil {
@ -70,6 +82,15 @@ func NewLevelPackWindow(config LevelPack) *ui.Window {
Height: height, Height: height,
Background: render.Grey, Background: render.Grey,
}) })
window.Handle(ui.CloseWindow, func(ed ui.EventData) error {
if config.OnCloseWindow != nil {
// fn := config.OnCloseWindow
// config.OnCloseWindow = nil
// fn()
config.OnCloseWindow()
}
return nil
})
config.window = window config.window = window
frame := ui.NewFrame("Window Body Frame") frame := ui.NewFrame("Window Body Frame")
@ -99,6 +120,8 @@ func NewLevelPackWindow(config LevelPack) *ui.Window {
config.makeIndexScreen(indexTab, width, height, lpFiles, packmap, func(screen string) { config.makeIndexScreen(indexTab, width, height, lpFiles, packmap, func(screen string) {
// Callback for user choosing a level pack. // Callback for user choosing a level pack.
// Hide the index screen and show the screen for this pack. // Hide the index screen and show the screen for this pack.
config.footerWithBackButton.Show()
config.footerWithCloseButton.Hide()
tabFrame.SetTab(screen) tabFrame.SetTab(screen)
}) })
for _, filename := range lpFiles { for _, filename := range lpFiles {
@ -109,20 +132,65 @@ func NewLevelPackWindow(config LevelPack) *ui.Window {
config.makeDetailScreen(tab, width, height, packmap[filename]) config.makeDetailScreen(tab, width, height, packmap[filename])
} }
// Close button. // Button toolbar at the bottom (Back, Close)
config.footerWithBackButton = ui.NewFrame("Button Bar w/ Back Button")
config.footerWithCloseButton = ui.NewFrame("Button Bar w/ Close Button Only")
window.Place(config.footerWithBackButton, ui.Place{
Bottom: 15,
Center: true,
})
window.Place(config.footerWithCloseButton, ui.Place{
Bottom: 15,
Center: true,
})
// Back button hidden by default.
config.footerWithBackButton.Hide()
// Back button (conditionally visible)
backButton := ui.NewButton("Back", ui.NewLabel(ui.Label{
Text: "« Back",
Font: balance.MenuFont,
}))
backButton.SetStyle(&balance.ButtonBabyBlue)
backButton.Handle(ui.Click, func(ed ui.EventData) error {
tabFrame.SetTab("LevelPacks")
config.footerWithBackButton.Hide()
config.footerWithCloseButton.Show()
return nil
})
config.Supervisor.Add(backButton)
config.footerWithBackButton.Pack(backButton, ui.Pack{
Side: ui.W,
PadX: 4,
})
// Close button (on both versions of the footer frame).
if config.OnCloseWindow != nil { if config.OnCloseWindow != nil {
closeBtn := ui.NewButton("Close Window", ui.NewLabel(ui.Label{ // Create two copies of the button, so we can parent one to each footer frame.
Text: "Close", makeCloseButton := func() *ui.Button {
Font: balance.MenuFont, closeBtn := ui.NewButton("Close Window", ui.NewLabel(ui.Label{
})) Text: "Close",
closeBtn.Handle(ui.Click, func(ed ui.EventData) error { Font: balance.MenuFont,
config.OnCloseWindow() }))
return nil closeBtn.Handle(ui.Click, func(ed ui.EventData) error {
config.OnCloseWindow()
return nil
})
config.Supervisor.Add(closeBtn)
return closeBtn
}
var (
button1 = makeCloseButton()
button2 = makeCloseButton()
)
// Add it to both frames.
config.footerWithBackButton.Pack(button1, ui.Pack{
Side: ui.W,
}) })
config.Supervisor.Add(closeBtn) config.footerWithCloseButton.Pack(button2, ui.Pack{
window.Place(closeBtn, ui.Place{ Side: ui.W,
Bottom: 15,
Center: true,
}) })
} }
@ -235,7 +303,7 @@ func (config LevelPack) makeIndexScreen(frame *ui.Frame, width, height int,
Pages: pages, Pages: pages,
PerPage: perPage, PerPage: perPage,
MaxPageButtons: maxPageButtons, MaxPageButtons: maxPageButtons,
Font: balance.MenuFont, Font: balance.PagerLargeFont,
OnChange: func(newPage, perPage int) { OnChange: func(newPage, perPage int) {
page = newPage page = newPage
log.Info("Page: %d, %d", page, perPage) log.Info("Page: %d, %d", page, perPage)
@ -271,18 +339,32 @@ func (config LevelPack) makeIndexScreen(frame *ui.Frame, width, height int,
// Detail screen for a given levelpack. // Detail screen for a given levelpack.
func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp *levelpack.LevelPack) *ui.Frame { func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp *levelpack.LevelPack) *ui.Frame {
var ( var (
buttonHeight = 40
buttonWidth = width - 40
page = 1 page = 1
perPage = 4 perPage = 2 // 2 for tall mobile, 3 for landscape
pages = int( pages = int(
math.Ceil( math.Ceil(
float64(len(lp.Levels)) / float64(perPage), float64(len(lp.Levels)) / float64(perPage),
), ),
) )
maxPageButtons = 10 maxPageButtons = 10
buttonHeight = 172
buttonWidth = 230
thumbnailName = balance.LevelScreenshotTinyFilename
thumbnailPadY = 46
) )
if config.isLandscape {
perPage = 3
pages = int(
math.Ceil(
float64(len(lp.Levels)) / float64(perPage),
),
)
buttonHeight = 172
thumbnailName = balance.LevelScreenshotTinyFilename
thumbnailPadY = 46
buttonWidth = (width / perPage) - 16 // pixel-pushing
}
// Load the padlock icon for locked levels. // Load the padlock icon for locked levels.
// If not loadable, won't be used in UI. // If not loadable, won't be used in UI.
@ -294,42 +376,13 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp
numUnlocked = lp.FreeLevels + numCompleted 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 // LevelPack Title label
label := ui.NewLabel(ui.Label{ label := ui.NewLabel(ui.Label{
Text: lp.Title, Text: lp.Title,
Font: balance.LabelFont, Font: balance.LabelFont,
}) })
frame.Pack(label, ui.Pack{ frame.Pack(label, ui.Pack{
Side: ui.NW, Side: ui.N,
PadX: 8,
PadY: 2, PadY: 2,
}) })
@ -359,12 +412,36 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp
}) })
} }
// Arranging the buttons into groups of 3, vertical or horizontal.
var packDir = ui.Pack{
Side: ui.N,
PadY: 2,
}
if config.isLandscape {
packDir = ui.Pack{
Side: ui.W,
PadX: 2,
}
}
buttonRow := ui.NewFrame("Level Buttons")
frame.Pack(buttonRow, ui.Pack{
Side: ui.N,
PadY: 4,
})
// Loop over all the levels in this pack. // Loop over all the levels in this pack.
var buttons []*ui.Button var buttons []*ui.Button
for i, level := range lp.Levels { for i, level := range lp.Levels {
level := level level := level
score := config.savegame.GetLevelScore(lp.Filename, level.Filename, level.UUID) score := config.savegame.GetLevelScore(lp.Filename, level.Filename, level.UUID)
// Load the level zip for its thumbnail image.
lvl, err := lp.GetLevel(level.Filename)
if err != nil {
log.Error("Couldn't GetLevel(%s) from LevelPack %s: %s", level.Filename, lp.Filename, err)
lvl = nil
}
// Make a frame to hold a complex button layout. // Make a frame to hold a complex button layout.
btnFrame := ui.NewFrame("Frame") btnFrame := ui.NewFrame("Frame")
btnFrame.Resize(render.Rect{ btnFrame.Resize(render.Rect{
@ -464,6 +541,16 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp
}) })
} }
// Level screenshot.
if lvl != nil {
if img, err := lvl.GetScreenshotImageAsUIImage(thumbnailName); err == nil {
btnFrame.Pack(img, ui.Pack{
Side: ui.N,
PadY: thumbnailPadY, // TODO: otherwise it overlaps the other labels :(
})
}
}
btn := ui.NewButton(level.Filename, btnFrame) btn := ui.NewButton(level.Filename, btnFrame)
btn.Handle(ui.Click, func(ed ui.EventData) error { btn.Handle(ui.Click, func(ed ui.EventData) error {
// Is this level locked? // Is this level locked?
@ -484,10 +571,7 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp
return nil return nil
}) })
frame.Pack(btn, ui.Pack{ buttonRow.Pack(btn, packDir)
Side: ui.N,
PadY: 2,
})
config.Supervisor.Add(btn) config.Supervisor.Add(btn)
if i > perPage-1 { if i > perPage-1 {
@ -502,7 +586,7 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp
Pages: pages, Pages: pages,
PerPage: perPage, PerPage: perPage,
MaxPageButtons: maxPageButtons, MaxPageButtons: maxPageButtons,
Font: balance.MenuFont, Font: balance.PagerLargeFont,
OnChange: func(newPage, perPage int) { OnChange: func(newPage, perPage int) {
page = newPage page = newPage
log.Info("Page: %d, %d", page, perPage) log.Info("Page: %d, %d", page, perPage)