Noah Petherbridge
1105d9312a
* Instead of a simple "cur. ver != latest ver" check, parse the Major, Minor and Patch components and do a detailed check. * So a x.x.1 release could be made for a specific platform that had a bad build, and it won't mind when it sees the latest version is the older x.x.0 build that other platforms had working fine.
420 lines
9.8 KiB
Go
420 lines
9.8 KiB
Go
package doodle
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
|
"git.kirsle.net/apps/doodle/pkg/level"
|
|
"git.kirsle.net/apps/doodle/pkg/license"
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
|
"git.kirsle.net/apps/doodle/pkg/native"
|
|
"git.kirsle.net/apps/doodle/pkg/scripting"
|
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
|
"git.kirsle.net/apps/doodle/pkg/uix"
|
|
"git.kirsle.net/apps/doodle/pkg/updater"
|
|
"git.kirsle.net/apps/doodle/pkg/windows"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/render/event"
|
|
"git.kirsle.net/go/ui"
|
|
)
|
|
|
|
// MainScene implements the main menu of Doodle.
|
|
type MainScene struct {
|
|
Supervisor *ui.Supervisor
|
|
|
|
// Background wallpaper canvas.
|
|
scripting *scripting.Supervisor
|
|
canvas *uix.Canvas
|
|
|
|
// UI components.
|
|
labelTitle *ui.Label
|
|
labelVersion *ui.Label
|
|
labelHint *ui.Label
|
|
frame *ui.Frame // Main button frame
|
|
btnRegister *ui.Button
|
|
winRegister *ui.Window
|
|
winSettings *ui.Window
|
|
|
|
// Update check variables.
|
|
updateButton *ui.Button
|
|
updateInfo updater.VersionInfo
|
|
|
|
// Lazy scroll variables. See LoopLazyScroll().
|
|
lazyScrollBounce bool
|
|
lazyScrollTrajectory render.Point
|
|
lazyScrollLastValue render.Point
|
|
}
|
|
|
|
// Name of the scene.
|
|
func (s *MainScene) Name() string {
|
|
return "Main"
|
|
}
|
|
|
|
// Setup the scene.
|
|
func (s *MainScene) Setup(d *Doodle) error {
|
|
s.Supervisor = ui.NewSupervisor()
|
|
|
|
if err := s.SetupDemoLevel(d); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Main title label
|
|
s.labelTitle = ui.NewLabel(ui.Label{
|
|
Text: branding.AppName,
|
|
Font: balance.TitleScreenFont,
|
|
})
|
|
s.labelTitle.Compute(d.Engine)
|
|
|
|
// Version label.
|
|
var shareware string
|
|
if !license.IsRegistered() {
|
|
shareware = " (shareware)"
|
|
}
|
|
ver := ui.NewLabel(ui.Label{
|
|
Text: fmt.Sprintf("v%s%s", branding.Version, shareware),
|
|
Font: render.Text{
|
|
Size: 18,
|
|
Color: render.Grey,
|
|
Shadow: render.Black,
|
|
},
|
|
})
|
|
ver.Compute(d.Engine)
|
|
s.labelVersion = ver
|
|
|
|
// Arrow Keys hint label (scroll the demo level).
|
|
s.labelHint = ui.NewLabel(ui.Label{
|
|
Text: "Hint: press the Arrow keys",
|
|
Font: render.Text{
|
|
Size: 16,
|
|
Color: render.Grey,
|
|
Shadow: render.Purple,
|
|
},
|
|
})
|
|
s.labelHint.Compute(d.Engine)
|
|
|
|
// "Update Available" button.
|
|
s.updateButton = ui.NewButton("Update Button", ui.NewLabel(ui.Label{
|
|
Text: "An update is available!",
|
|
Font: render.Text{
|
|
FontFilename: "DejaVuSans-Bold.ttf",
|
|
Size: 16,
|
|
Color: render.Blue,
|
|
Padding: 4,
|
|
},
|
|
}))
|
|
s.updateButton.Handle(ui.Click, func(ed ui.EventData) error {
|
|
native.OpenURL(s.updateInfo.DownloadURL)
|
|
return nil
|
|
})
|
|
s.updateButton.Compute(d.Engine)
|
|
s.updateButton.Hide()
|
|
s.Supervisor.Add(s.updateButton)
|
|
|
|
// Register button.
|
|
s.btnRegister = ui.NewButton("Register", ui.NewLabel(ui.Label{
|
|
Text: "Register Game",
|
|
Font: balance.LabelFont,
|
|
}))
|
|
s.btnRegister.SetStyle(&balance.ButtonPrimary)
|
|
s.btnRegister.Handle(ui.Click, func(ed ui.EventData) error {
|
|
if s.winRegister == nil {
|
|
cfg := windows.License{
|
|
Supervisor: s.Supervisor,
|
|
Engine: d.Engine,
|
|
OnCancel: func() {
|
|
s.winRegister.Hide()
|
|
},
|
|
}
|
|
cfg.OnLicensed = func() {
|
|
// License status has changed, reload the window!
|
|
if s.winRegister != nil {
|
|
s.winRegister.Hide()
|
|
}
|
|
s.winRegister = windows.MakeLicenseWindow(d.width, d.height, cfg)
|
|
}
|
|
|
|
cfg.OnLicensed()
|
|
}
|
|
s.winRegister.Show()
|
|
return nil
|
|
})
|
|
s.btnRegister.Compute(d.Engine)
|
|
s.Supervisor.Add(s.btnRegister)
|
|
|
|
if license.IsRegistered() {
|
|
s.btnRegister.Hide()
|
|
}
|
|
|
|
// Main UI button frame.
|
|
frame := ui.NewFrame("frame")
|
|
s.frame = frame
|
|
|
|
var buttons = []struct {
|
|
Name string
|
|
Func func()
|
|
}{
|
|
// {
|
|
// Name: "Story Mode",
|
|
// Func: d.GotoStoryMenu,
|
|
// },
|
|
{
|
|
Name: "Play a Level",
|
|
Func: d.GotoPlayMenu,
|
|
},
|
|
{
|
|
Name: "Create a New Level",
|
|
Func: d.GotoNewMenu,
|
|
},
|
|
{
|
|
Name: "Edit a Level",
|
|
Func: d.GotoLoadMenu,
|
|
},
|
|
{
|
|
Name: "Settings",
|
|
Func: func() {
|
|
if s.winSettings == nil {
|
|
s.winSettings = d.MakeSettingsWindow(s.Supervisor)
|
|
}
|
|
s.winSettings.Show()
|
|
},
|
|
},
|
|
}
|
|
for _, button := range buttons {
|
|
button := button
|
|
btn := ui.NewButton(button.Name, ui.NewLabel(ui.Label{
|
|
Text: button.Name,
|
|
Font: balance.StatusFont,
|
|
}))
|
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
|
button.Func()
|
|
return nil
|
|
})
|
|
s.Supervisor.Add(btn)
|
|
frame.Pack(btn, ui.Pack{
|
|
Side: ui.N,
|
|
PadY: 8,
|
|
// Fill: true,
|
|
FillX: true,
|
|
})
|
|
}
|
|
|
|
// Check for update in the background.
|
|
go s.checkUpdate()
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkUpdate checks for a version update and shows the button.
|
|
func (s *MainScene) checkUpdate() {
|
|
if shmem.OfflineMode {
|
|
log.Info("OfflineMode: skip updates check")
|
|
return
|
|
}
|
|
|
|
info, err := updater.Check()
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
return
|
|
}
|
|
|
|
if info.IsNewerVersionThan(branding.Version) {
|
|
s.updateInfo = info
|
|
s.updateButton.Show()
|
|
}
|
|
}
|
|
|
|
// SetupDemoLevel configures the wallpaper behind the New screen,
|
|
// which demos a title screen demo level.
|
|
func (s *MainScene) SetupDemoLevel(d *Doodle) error {
|
|
// Set up the background wallpaper canvas.
|
|
s.canvas = uix.NewCanvas(100, false)
|
|
s.canvas.Scrollable = true
|
|
s.canvas.Resize(render.Rect{
|
|
W: d.width,
|
|
H: d.height,
|
|
})
|
|
|
|
s.scripting = scripting.NewSupervisor()
|
|
s.canvas.SetScriptSupervisor(s.scripting)
|
|
|
|
// Title screen level to load.
|
|
if lvl, err := level.LoadFile(balance.DemoLevelName); err == nil {
|
|
s.canvas.LoadLevel(d.Engine, lvl)
|
|
s.canvas.InstallActors(lvl.Actors)
|
|
|
|
// Load all actor scripts.
|
|
if err := s.scripting.InstallScripts(lvl); err != nil {
|
|
log.Error("Error with title screen level scripts: %s", err)
|
|
}
|
|
|
|
// Run all actors scripts main function to start them off.
|
|
if err := s.canvas.InstallScripts(); err != nil {
|
|
log.Error("Error running actor main() functions: %s", err)
|
|
}
|
|
} else {
|
|
log.Error("Error loading demo level %s: %s", balance.DemoLevelName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Loop the editor scene.
|
|
func (s *MainScene) Loop(d *Doodle, ev *event.State) error {
|
|
s.Supervisor.Loop(ev)
|
|
|
|
if err := s.scripting.Loop(); err != nil {
|
|
log.Error("MainScene.Loop: scripting.Loop: %s", err)
|
|
}
|
|
|
|
// Lazily scroll the canvas around, slowly.
|
|
s.LoopLazyScroll()
|
|
|
|
s.canvas.Loop(ev)
|
|
|
|
if ev.WindowResized {
|
|
w, h := d.Engine.WindowSize()
|
|
d.width = w
|
|
d.height = h
|
|
log.Info("Resized to %dx%d", d.width, d.height)
|
|
s.canvas.Resize(render.Rect{
|
|
W: d.width,
|
|
H: d.height,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoopLazyScroll gently scrolls the title screen demo level, called each Loop.
|
|
func (s *MainScene) LoopLazyScroll() {
|
|
// The v1 basic sauce algorithm:
|
|
// 1. We scroll diagonally downwards and rightwards.
|
|
// 2. When we scroll downwards far enough, we change direction.
|
|
// Make a zigzag pattern.
|
|
// 3. When we reach the right bound of the level
|
|
// OR some max number of px into an unbounded level:
|
|
// enter a simple ball bouncing mode like a screensaver.
|
|
var (
|
|
zigzagMaxHeight = 512
|
|
maxScrollX = zigzagMaxHeight * 2
|
|
lastScrollValue = s.lazyScrollLastValue
|
|
currentScroll = s.canvas.Scroll
|
|
)
|
|
|
|
// So we have two states:
|
|
// - Zigzag state (default)
|
|
// - Bounce state (when we hit a wall)
|
|
if !s.lazyScrollBounce {
|
|
// Zigzag state.
|
|
s.lazyScrollTrajectory = render.Point{
|
|
X: -1, // down and right
|
|
Y: -1,
|
|
}
|
|
|
|
// When we've gone far enough X, it's also far enough Y.
|
|
if currentScroll.X < -zigzagMaxHeight {
|
|
s.lazyScrollTrajectory.Y = 1 // go back up
|
|
}
|
|
|
|
// Have we gotten stuck in a corner? (ending the zigzag phase, for bounded levels)
|
|
if currentScroll.X < 0 && (currentScroll == lastScrollValue) || currentScroll.X < -maxScrollX {
|
|
log.Debug("LoopLazyScroll: Ending zigzag phase, enter bounce phase")
|
|
s.lazyScrollBounce = true
|
|
s.lazyScrollTrajectory = render.Point{
|
|
X: -1,
|
|
Y: -1,
|
|
}
|
|
}
|
|
} else {
|
|
// Lazy bounce algorithm.
|
|
if currentScroll.Y == lastScrollValue.Y {
|
|
log.Debug("LoopLazyScroll: Hit a floor/ceiling")
|
|
s.lazyScrollTrajectory.Y = -s.lazyScrollTrajectory.Y
|
|
}
|
|
if currentScroll.X == lastScrollValue.X {
|
|
log.Debug("LoopLazyScroll: Hit the side of the map!")
|
|
s.lazyScrollTrajectory.X = -s.lazyScrollTrajectory.X
|
|
}
|
|
}
|
|
|
|
// Check the scroll.
|
|
s.lazyScrollLastValue = currentScroll
|
|
s.canvas.ScrollBy(s.lazyScrollTrajectory)
|
|
}
|
|
|
|
// Draw the pixels on this frame.
|
|
func (s *MainScene) Draw(d *Doodle) error {
|
|
// Clear the canvas and fill it with white.
|
|
d.Engine.Clear(render.White)
|
|
|
|
s.canvas.Present(d.Engine, render.Origin)
|
|
|
|
// Draw a sheen over the level for clarity.
|
|
d.Engine.DrawBox(render.RGBA(255, 255, 254, 96), render.Rect{
|
|
X: 0,
|
|
Y: 0,
|
|
W: d.width,
|
|
H: d.height,
|
|
})
|
|
|
|
// Draw out bounding boxes.
|
|
if DebugCollision {
|
|
for _, actor := range s.canvas.Actors() {
|
|
d.DrawCollisionBox(s.canvas, actor)
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
|
|
// Version label
|
|
s.labelVersion.MoveTo(render.Point{
|
|
X: (d.width / 2) - (s.labelVersion.Size().W / 2),
|
|
Y: s.labelTitle.Point().Y + s.labelTitle.Size().H + 8,
|
|
})
|
|
s.labelVersion.Present(d.Engine, s.labelVersion.Point())
|
|
|
|
// 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())
|
|
|
|
// 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.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())
|
|
|
|
// 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())
|
|
|
|
// Present supervised windows.
|
|
s.Supervisor.Present(d.Engine)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Destroy the scene.
|
|
func (s *MainScene) Destroy() error {
|
|
return nil
|
|
}
|