WIP LevelPack UI + Landscape Mode Title Screen
The title screen is now responsive to landscape mode. If the window is not tall enough to show all the menu buttons (~600px) it will switch to a horizontal layout with the title on the left and buttons on the right. WIP "Story Mode" button that brings up a Level Packs selection window.
This commit is contained in:
parent
a75b7208ca
commit
678326540b
|
@ -12,6 +12,10 @@ 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
|
||||||
|
|
|
@ -15,4 +15,5 @@ const (
|
||||||
const (
|
const (
|
||||||
LevelExt = ".level"
|
LevelExt = ".level"
|
||||||
DoodadExt = ".doodad"
|
DoodadExt = ".doodad"
|
||||||
|
LevelPackExt = ".levelpack"
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ var (
|
||||||
SystemDoodadsPath = filepath.Join("assets", "doodads")
|
SystemDoodadsPath = filepath.Join("assets", "doodads")
|
||||||
SystemLevelsPath = filepath.Join("assets", "levels")
|
SystemLevelsPath = filepath.Join("assets", "levels")
|
||||||
SystemCampaignsPath = filepath.Join("assets", "campaigns")
|
SystemCampaignsPath = filepath.Join("assets", "campaigns")
|
||||||
|
SystemLevelPacksPath = filepath.Join("assets", "levelpacks")
|
||||||
)
|
)
|
||||||
|
|
||||||
// MakeHeader creates the binary file header.
|
// MakeHeader creates the binary file header.
|
||||||
|
|
|
@ -7,8 +7,14 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/assets"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/filesystem"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LevelPack describes the contents of a levelpack file.
|
// LevelPack describes the contents of a levelpack file.
|
||||||
|
@ -63,6 +69,42 @@ func LoadFile(filename string) (LevelPack, error) {
|
||||||
return lp, nil
|
return lp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListFiles lists all the discoverable levelpack files, starting from
|
||||||
|
// the game's built-ins all the way to user levelpacks.
|
||||||
|
func ListFiles() ([]string, error) {
|
||||||
|
var names []string
|
||||||
|
|
||||||
|
// List levelpacks embedded into the binary.
|
||||||
|
if files, err := assets.AssetDir("assets/levelpacks"); err == nil {
|
||||||
|
names = append(names, files...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM stops here, no filesystem access.
|
||||||
|
if runtime.GOOS == "js" {
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read system-level levelpacks.
|
||||||
|
files, _ := ioutil.ReadDir(filesystem.SystemLevelPacksPath)
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
if strings.HasSuffix(name, enum.LevelPackExt) {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append user levelpacks.
|
||||||
|
files, _ = ioutil.ReadDir(userdir.LevelPackDirectory)
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
if strings.HasSuffix(name, enum.LevelPackExt) {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteFile saves the metadata to a .json file on disk.
|
// WriteFile saves the metadata to a .json file on disk.
|
||||||
func (l LevelPack) WriteFile(filename string) error {
|
func (l LevelPack) WriteFile(filename string) error {
|
||||||
out, err := json.Marshal(l)
|
out, err := json.Marshal(l)
|
||||||
|
|
|
@ -38,6 +38,7 @@ type MainScene struct {
|
||||||
btnRegister *ui.Button
|
btnRegister *ui.Button
|
||||||
winRegister *ui.Window
|
winRegister *ui.Window
|
||||||
winSettings *ui.Window
|
winSettings *ui.Window
|
||||||
|
winLevelPacks *ui.Window
|
||||||
|
|
||||||
// Update check variables.
|
// Update check variables.
|
||||||
updateButton *ui.Button
|
updateButton *ui.Button
|
||||||
|
@ -47,6 +48,12 @@ type MainScene struct {
|
||||||
lazyScrollBounce bool
|
lazyScrollBounce bool
|
||||||
lazyScrollTrajectory render.Point
|
lazyScrollTrajectory render.Point
|
||||||
lazyScrollLastValue render.Point
|
lazyScrollLastValue render.Point
|
||||||
|
|
||||||
|
// Landscape mode: if the screen isn't tall enough to see the main
|
||||||
|
// menu we redo the layout to be landscape friendly. NOTE: this only
|
||||||
|
// happens one time, and does not re-adapt when the window is made
|
||||||
|
// tall enough again.
|
||||||
|
landscapeMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name of the scene.
|
// Name of the scene.
|
||||||
|
@ -161,10 +168,22 @@ func (s *MainScene) Setup(d *Doodle) error {
|
||||||
Func func()
|
Func func()
|
||||||
Style *style.Button
|
Style *style.Button
|
||||||
}{
|
}{
|
||||||
// {
|
{
|
||||||
// Name: "Story Mode",
|
Name: "Story Mode",
|
||||||
// Func: d.GotoStoryMenu,
|
Func: func() {
|
||||||
// },
|
if s.winLevelPacks == nil {
|
||||||
|
s.winLevelPacks = windows.NewLevelPackWindow(windows.LevelPack{
|
||||||
|
Supervisor: s.Supervisor,
|
||||||
|
Engine: d.Engine,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
s.winLevelPacks.MoveTo(render.Point{
|
||||||
|
X: (d.width / 2) - (s.winLevelPacks.Size().W / 2),
|
||||||
|
Y: (d.height / 2) - (s.winLevelPacks.Size().H / 2),
|
||||||
|
})
|
||||||
|
s.winLevelPacks.Show()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "Play a Level",
|
Name: "Play a Level",
|
||||||
Func: d.GotoPlayMenu,
|
Func: d.GotoPlayMenu,
|
||||||
|
@ -229,6 +248,10 @@ func (s *MainScene) Setup(d *Doodle) error {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Trigger our "Window Resized" function so we can check if the
|
||||||
|
// layout needs to be switched to landscape mode for mobile.
|
||||||
|
s.Resized(d.width, d.height)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,16 +333,121 @@ func (s *MainScene) Loop(d *Doodle, ev *event.State) error {
|
||||||
w, h := d.Engine.WindowSize()
|
w, h := d.Engine.WindowSize()
|
||||||
d.width = w
|
d.width = w
|
||||||
d.height = h
|
d.height = h
|
||||||
log.Info("Resized to %dx%d", d.width, d.height)
|
s.Resized(w, h)
|
||||||
s.canvas.Resize(render.Rect{
|
|
||||||
W: d.width,
|
|
||||||
H: d.height,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resized the app window.
|
||||||
|
func (s *MainScene) Resized(width, height int) {
|
||||||
|
log.Info("Resized to %dx%d", width, height)
|
||||||
|
|
||||||
|
// If the height is not tall enough for the menu, switch to the horizontal layout.
|
||||||
|
if height < balance.TitleScreenResponsiveHeight {
|
||||||
|
log.Error("Switch to landscape mode")
|
||||||
|
s.landscapeMode = true
|
||||||
|
} else {
|
||||||
|
s.landscapeMode = false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.canvas.Resize(render.Rect{
|
||||||
|
W: width,
|
||||||
|
H: height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move things into position for the main menu. This function arranges
|
||||||
|
// the Title, Subtitle, Buttons, etc. into screen relative positions every
|
||||||
|
// tick. This function sets their 'default' values, but if the window is
|
||||||
|
// not tall enough and needs the landscape orientation, positionMenuLandscape()
|
||||||
|
// will override these defaults.
|
||||||
|
func (s *MainScene) positionMenuPortrait(d *Doodle) {
|
||||||
|
// App title label.
|
||||||
|
s.labelTitle.MoveTo(render.Point{
|
||||||
|
X: (d.width / 2) - (s.labelTitle.Size().W / 2),
|
||||||
|
Y: 120,
|
||||||
|
})
|
||||||
|
|
||||||
|
// App subtitle label (byline).
|
||||||
|
s.labelSubtitle.MoveTo(render.Point{
|
||||||
|
X: (d.width / 2) - (s.labelSubtitle.Size().W / 2),
|
||||||
|
Y: s.labelTitle.Point().Y + s.labelTitle.Size().H + 8,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Version label
|
||||||
|
s.labelVersion.MoveTo(render.Point{
|
||||||
|
X: (d.width) - (s.labelVersion.Size().W) - 20,
|
||||||
|
Y: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hint label.
|
||||||
|
s.labelHint.MoveTo(render.Point{
|
||||||
|
X: (d.width / 2) - (s.labelHint.Size().W / 2),
|
||||||
|
Y: d.height - s.labelHint.Size().H - 32,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update button.
|
||||||
|
s.updateButton.MoveTo(render.Point{
|
||||||
|
X: 24,
|
||||||
|
Y: d.height - s.updateButton.Size().H - 24,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Button frame.
|
||||||
|
s.frame.MoveTo(render.Point{
|
||||||
|
X: (d.width / 2) - (s.frame.Size().W / 2),
|
||||||
|
Y: 260,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register button.
|
||||||
|
s.btnRegister.MoveTo(render.Point{
|
||||||
|
X: d.width - s.btnRegister.Size().W - 24,
|
||||||
|
Y: d.height - s.btnRegister.Size().H - 24,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MainScene) positionMenuLandscape(d *Doodle) {
|
||||||
|
s.positionMenuPortrait(d)
|
||||||
|
|
||||||
|
var (
|
||||||
|
col1 = render.Rect{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
W: d.width / 2,
|
||||||
|
H: d.height,
|
||||||
|
}
|
||||||
|
col2 = render.Rect{
|
||||||
|
X: d.width,
|
||||||
|
Y: 0,
|
||||||
|
W: d.width - col1.W,
|
||||||
|
H: d.height,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Title and subtitle move to the left.
|
||||||
|
s.labelTitle.MoveTo(render.Point{
|
||||||
|
X: (col1.W / 2) - (s.labelTitle.Size().W / 2),
|
||||||
|
Y: s.labelTitle.Point().Y,
|
||||||
|
})
|
||||||
|
s.labelSubtitle.MoveTo(render.Point{
|
||||||
|
X: (col1.W / 2) - (s.labelSubtitle.Size().W / 2),
|
||||||
|
Y: s.labelTitle.Point().Y + s.labelTitle.Size().H + 8,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Button frame to the right.
|
||||||
|
s.frame.MoveTo(render.Point{
|
||||||
|
X: (col2.X+col2.W)/2 - (s.frame.Size().W / 2),
|
||||||
|
Y: (d.height / 2) - (s.frame.Size().H / 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register button to the top left.
|
||||||
|
// TODO: not ideal, move into main button list?
|
||||||
|
s.btnRegister.MoveTo(render.Point{
|
||||||
|
X: 20,
|
||||||
|
Y: 20,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// LoopLazyScroll gently scrolls the title screen demo level, called each Loop.
|
// LoopLazyScroll gently scrolls the title screen demo level, called each Loop.
|
||||||
func (s *MainScene) LoopLazyScroll() {
|
func (s *MainScene) LoopLazyScroll() {
|
||||||
// The v1 basic sauce algorithm:
|
// The v1 basic sauce algorithm:
|
||||||
|
@ -399,53 +527,32 @@ func (s *MainScene) Draw(d *Doodle) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arrange the main widgets by Portrait or Landscape mode.
|
||||||
|
if s.landscapeMode {
|
||||||
|
s.positionMenuLandscape(d)
|
||||||
|
} else {
|
||||||
|
s.positionMenuPortrait(d)
|
||||||
|
}
|
||||||
|
|
||||||
// App title label.
|
// App title label.
|
||||||
s.labelTitle.MoveTo(render.Point{
|
|
||||||
X: (d.width / 2) - (s.labelTitle.Size().W / 2),
|
|
||||||
Y: 120,
|
|
||||||
})
|
|
||||||
s.labelTitle.Present(d.Engine, s.labelTitle.Point())
|
s.labelTitle.Present(d.Engine, s.labelTitle.Point())
|
||||||
|
|
||||||
// App subtitle label (byline).
|
// App subtitle label (byline).
|
||||||
s.labelSubtitle.MoveTo(render.Point{
|
|
||||||
X: (d.width / 2) - (s.labelSubtitle.Size().W / 2),
|
|
||||||
Y: s.labelTitle.Point().Y + s.labelTitle.Size().H + 8,
|
|
||||||
})
|
|
||||||
s.labelSubtitle.Present(d.Engine, s.labelSubtitle.Point())
|
s.labelSubtitle.Present(d.Engine, s.labelSubtitle.Point())
|
||||||
|
|
||||||
// Version label
|
// Version label
|
||||||
s.labelVersion.MoveTo(render.Point{
|
|
||||||
X: (d.width) - (s.labelVersion.Size().W) - 20,
|
|
||||||
Y: 20,
|
|
||||||
})
|
|
||||||
s.labelVersion.Present(d.Engine, s.labelVersion.Point())
|
s.labelVersion.Present(d.Engine, s.labelVersion.Point())
|
||||||
|
|
||||||
// Hint label.
|
// Hint label.
|
||||||
s.labelHint.MoveTo(render.Point{
|
|
||||||
X: (d.width / 2) - (s.labelHint.Size().W / 2),
|
|
||||||
Y: d.height - s.labelHint.Size().H - 32,
|
|
||||||
})
|
|
||||||
s.labelHint.Present(d.Engine, s.labelHint.Point())
|
s.labelHint.Present(d.Engine, s.labelHint.Point())
|
||||||
|
|
||||||
// Update button.
|
// Update button.
|
||||||
s.updateButton.MoveTo(render.Point{
|
|
||||||
X: 24,
|
|
||||||
Y: d.height - s.updateButton.Size().H - 24,
|
|
||||||
})
|
|
||||||
s.updateButton.Present(d.Engine, s.updateButton.Point())
|
s.updateButton.Present(d.Engine, s.updateButton.Point())
|
||||||
|
|
||||||
s.frame.Compute(d.Engine)
|
s.frame.Compute(d.Engine)
|
||||||
s.frame.MoveTo(render.Point{
|
|
||||||
X: (d.width / 2) - (s.frame.Size().W / 2),
|
|
||||||
Y: 260,
|
|
||||||
})
|
|
||||||
s.frame.Present(d.Engine, s.frame.Point())
|
s.frame.Present(d.Engine, s.frame.Point())
|
||||||
|
|
||||||
// Register button.
|
// Register button.
|
||||||
s.btnRegister.MoveTo(render.Point{
|
|
||||||
X: d.width - s.btnRegister.Size().W - 24,
|
|
||||||
Y: d.height - s.btnRegister.Size().H - 24,
|
|
||||||
})
|
|
||||||
s.btnRegister.Present(d.Engine, s.btnRegister.Point())
|
s.btnRegister.Present(d.Engine, s.btnRegister.Point())
|
||||||
|
|
||||||
// Present supervised windows.
|
// Present supervised windows.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/windows"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/render/event"
|
"git.kirsle.net/go/render/event"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
@ -23,7 +24,7 @@ type StoryScene struct {
|
||||||
// UI widgets.
|
// UI widgets.
|
||||||
supervisor *ui.Supervisor
|
supervisor *ui.Supervisor
|
||||||
campaignFrame *ui.Frame // Select a Campaign screen
|
campaignFrame *ui.Frame // Select a Campaign screen
|
||||||
levelSelectFrame *ui.Frame // Select a level in the campaign screen
|
levelSelectFrame *ui.Window // Select a level in the campaign screen
|
||||||
|
|
||||||
// Pointer to the currently active frame.
|
// Pointer to the currently active frame.
|
||||||
activeFrame *ui.Frame
|
activeFrame *ui.Frame
|
||||||
|
@ -59,7 +60,13 @@ func (s *StoryScene) Setup(d *Doodle) error {
|
||||||
|
|
||||||
// Set up the sub-screens of this scene.
|
// Set up the sub-screens of this scene.
|
||||||
s.campaignFrame = s.setupCampaignFrame()
|
s.campaignFrame = s.setupCampaignFrame()
|
||||||
s.levelSelectFrame = s.setupLevelSelectFrame()
|
s.levelSelectFrame = windows.NewLevelPackWindow(windows.LevelPack{
|
||||||
|
Supervisor: s.supervisor,
|
||||||
|
Engine: d.Engine,
|
||||||
|
|
||||||
|
OnPlayLevel: func(levelpack, filename string) {},
|
||||||
|
})
|
||||||
|
s.levelSelectFrame.Show()
|
||||||
|
|
||||||
s.activeFrame = s.campaignFrame
|
s.activeFrame = s.campaignFrame
|
||||||
|
|
||||||
|
@ -100,13 +107,6 @@ func (s *StoryScene) setupCampaignFrame() *ui.Frame {
|
||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupLevelSelectFrame sets up the Level Select screen.
|
|
||||||
func (s *StoryScene) setupLevelSelectFrame() *ui.Frame {
|
|
||||||
var frame = ui.NewFrame("List Frame")
|
|
||||||
|
|
||||||
return frame
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop the story scene.
|
// Loop the story scene.
|
||||||
func (s *StoryScene) Loop(d *Doodle, ev *event.State) error {
|
func (s *StoryScene) Loop(d *Doodle, ev *event.State) error {
|
||||||
s.supervisor.Loop(ev)
|
s.supervisor.Loop(ev)
|
||||||
|
@ -135,6 +135,8 @@ func (s *StoryScene) Draw(d *Doodle) error {
|
||||||
// Draw the active screen.
|
// Draw the active screen.
|
||||||
s.activeFrame.Present(d.Engine, render.Origin)
|
s.activeFrame.Present(d.Engine, render.Origin)
|
||||||
|
|
||||||
|
s.supervisor.Present(d.Engine)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ var (
|
||||||
|
|
||||||
ProfileDirectory string
|
ProfileDirectory string
|
||||||
LevelDirectory string
|
LevelDirectory string
|
||||||
|
LevelPackDirectory string
|
||||||
DoodadDirectory string
|
DoodadDirectory string
|
||||||
CampaignDirectory string
|
CampaignDirectory string
|
||||||
ScreenshotDirectory string
|
ScreenshotDirectory string
|
||||||
|
@ -35,6 +36,7 @@ func init() {
|
||||||
// Profile directory contains the user's levels and doodads.
|
// Profile directory contains the user's levels and doodads.
|
||||||
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
|
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
|
||||||
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
||||||
|
LevelPackDirectory = configdir.LocalConfig(ConfigDirectoryName, "levelpacks")
|
||||||
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
||||||
CampaignDirectory = configdir.LocalConfig(ConfigDirectoryName, "campaigns")
|
CampaignDirectory = configdir.LocalConfig(ConfigDirectoryName, "campaigns")
|
||||||
ScreenshotDirectory = configdir.LocalConfig(ConfigDirectoryName, "screenshots")
|
ScreenshotDirectory = configdir.LocalConfig(ConfigDirectoryName, "screenshots")
|
||||||
|
@ -47,6 +49,7 @@ func init() {
|
||||||
// WASM: do not make paths in wasm.
|
// WASM: do not make paths in wasm.
|
||||||
if runtime.GOOS != "js" {
|
if runtime.GOOS != "js" {
|
||||||
configdir.MakePath(LevelDirectory)
|
configdir.MakePath(LevelDirectory)
|
||||||
|
configdir.MakePath(LevelPackDirectory)
|
||||||
configdir.MakePath(DoodadDirectory)
|
configdir.MakePath(DoodadDirectory)
|
||||||
configdir.MakePath(CampaignDirectory)
|
configdir.MakePath(CampaignDirectory)
|
||||||
configdir.MakePath(FontDirectory)
|
configdir.MakePath(FontDirectory)
|
||||||
|
|
258
pkg/windows/levelpack_open.go
Normal file
258
pkg/windows/levelpack_open.go
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
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/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(levelpack, filename string)
|
||||||
|
|
||||||
|
// Internal variables
|
||||||
|
window *ui.Window
|
||||||
|
gotoIndex func() // return to index screen
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = 300
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
// We'll divide this window into "Screens", where the default
|
||||||
|
// screen shows the available level packs and then each level
|
||||||
|
// pack gets its own screen showing its levels.
|
||||||
|
var indexScreen *ui.Frame
|
||||||
|
config.gotoIndex = func() {
|
||||||
|
indexScreen.Show()
|
||||||
|
}
|
||||||
|
indexScreen = config.makeIndexScreen(width, height, func(screen *ui.Frame) {
|
||||||
|
// Callback for user choosing a level pack.
|
||||||
|
// Hide the index screen and show the screen for this pack.
|
||||||
|
indexScreen.Hide()
|
||||||
|
screen.Show()
|
||||||
|
})
|
||||||
|
window.Pack(indexScreen, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
Fill: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.Supervise(config.Supervisor)
|
||||||
|
window.Hide()
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index screen for the LevelPack window.
|
||||||
|
func (config LevelPack) makeIndexScreen(width, height int, onChoose func(*ui.Frame)) *ui.Frame {
|
||||||
|
var (
|
||||||
|
buttonHeight = 60 // height of each LevelPack button
|
||||||
|
buttonWidth = width - 40
|
||||||
|
|
||||||
|
// pagination values
|
||||||
|
page = 1
|
||||||
|
pages int
|
||||||
|
perPage = 3
|
||||||
|
maxPageButtons = 10
|
||||||
|
)
|
||||||
|
frame := ui.NewFrame("Index Screen")
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the available .levelpack files.
|
||||||
|
lpFiles, err := levelpack.ListFiles()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't list levelpack files: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pages = int(
|
||||||
|
math.Ceil(
|
||||||
|
float64(len(lpFiles)) / float64(perPage),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var buttons []*ui.Button
|
||||||
|
for i, filename := range lpFiles {
|
||||||
|
lp, err := levelpack.LoadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't read %s: %s", filename, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = lp
|
||||||
|
|
||||||
|
// 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("[%d levels]", len(lp.Levels)),
|
||||||
|
Font: balance.MenuFont,
|
||||||
|
})
|
||||||
|
btnFrame.Pack(numLevels, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate the detail screen (Frame) for this level pack.
|
||||||
|
// Should the user click our button, this screen is shown.
|
||||||
|
screen := config.makeDetailScreen(width, height, lp)
|
||||||
|
screen.Hide()
|
||||||
|
config.window.Pack(screen, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
Fill: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
button := ui.NewButton(filename, btnFrame)
|
||||||
|
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
onChoose(screen)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.Pack(button, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadY: 2,
|
||||||
|
})
|
||||||
|
config.Supervisor.Add(button)
|
||||||
|
|
||||||
|
if i > perPage {
|
||||||
|
button.Hide()
|
||||||
|
}
|
||||||
|
buttons = append(buttons, button)
|
||||||
|
}
|
||||||
|
|
||||||
|
pager := ui.NewPager(ui.Pager{
|
||||||
|
Name: "LevelPack Pager",
|
||||||
|
Page: page,
|
||||||
|
Pages: pages,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail screen for a given levelpack.
|
||||||
|
func (config LevelPack) makeDetailScreen(width, height int, lp levelpack.LevelPack) *ui.Frame {
|
||||||
|
frame := ui.NewFrame("Detail Screen")
|
||||||
|
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: "HELLO " + lp.Title,
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
})
|
||||||
|
frame.Pack(label, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadX: 8,
|
||||||
|
PadY: 8,
|
||||||
|
})
|
||||||
|
|
||||||
|
backButton := ui.NewButton("Back", ui.NewLabel(ui.Label{
|
||||||
|
Text: "< Back to Level Packs",
|
||||||
|
Font: ui.MenuFont,
|
||||||
|
}))
|
||||||
|
backButton.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
frame.Hide()
|
||||||
|
config.gotoIndex()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
config.Supervisor.Add(backButton)
|
||||||
|
frame.Pack(backButton, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadY: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user