Add app version/update check to the Main Scene
This commit is contained in:
parent
8965a7d86a
commit
cd31868a13
|
@ -1,6 +1,92 @@
|
||||||
# Doodad Ideas and Implementation Notes
|
# Doodad Ideas and Implementation Notes
|
||||||
|
|
||||||
## Crumbly Floor
|
## Warp Doors
|
||||||
|
|
||||||
|
Warp Doors would connect two places in the same level and allow the player to
|
||||||
|
instantly travel between them.
|
||||||
|
|
||||||
|
Usage: drag two Warp Doors into the level and Link them together.
|
||||||
|
|
||||||
|
When the player activates a Warp Door (i.e. press Up key in front of it), it'd
|
||||||
|
play an open animation and the player would be warped to the door it's linked to.
|
||||||
|
|
||||||
|
A few ideas of variants on the Warp Door:
|
||||||
|
|
||||||
|
* Two-state Warp Door (orange and blue). They'd work like the two-state blocks;
|
||||||
|
if the orange blocks are in dotted-outline mode, the Orange Door is too and
|
||||||
|
can not be opened while the Blue Door can. When the two-state button is pushed,
|
||||||
|
orange dotted-outline doors become solid and Blue Doors become dotted-outline.
|
||||||
|
* Locked door, requiring a key to unlock it (one-time) before entering. If the
|
||||||
|
linked door is also locked, it becomes unlocked when the player travels thru
|
||||||
|
it. If a Locked Door is linked to a normal one, players can travel into the
|
||||||
|
normal door and out the Locked Door but not back again without a key.
|
||||||
|
|
||||||
|
## Clocks
|
||||||
|
|
||||||
|
Clock doodads would emit timer events globally to be responded to by other
|
||||||
|
doodads. Clocks would come in a few varieties (i.e. 5 seconds, 10 seconds,
|
||||||
|
30 seconds).
|
||||||
|
|
||||||
|
Each clock's sprite would consist of a clock symbol and the number of seconds
|
||||||
|
for the clock. Each sprite would be a distinct color.
|
||||||
|
|
||||||
|
Clock sprites are **ONLY** visible in the Editor Mode; in Play Mode, the sprite
|
||||||
|
hides itself immediately. Clock scripts will emit global pub/sub events, distinct
|
||||||
|
to each clock, on an interval. For example the 10-second clock would emit an
|
||||||
|
event named "clock:10s" every 10 seconds. Interested doodad scripts could
|
||||||
|
subscribe to that clock signal to run their own logic.
|
||||||
|
|
||||||
|
## Small Key Locked Doors
|
||||||
|
|
||||||
|
We already have color-coded Locked Doors (blue, red, yellow, green) where the
|
||||||
|
player needs to pick up the matching-color key, and then they can open ALL DOORS
|
||||||
|
of the matching color. (Colored keys are multiple-use items).
|
||||||
|
|
||||||
|
Add a new "Small Key Locked Door" which uses consumable keys: when the player
|
||||||
|
picks up a Small Key they can unlock one door with it, which consumes the key.
|
||||||
|
The player then needs another Small Key to unlock another door. The player can
|
||||||
|
carry multiple Small Keys at the same time.
|
||||||
|
|
||||||
|
## Movable Platform
|
||||||
|
|
||||||
|
Add a platform that the player can ride on that moves from one point to another.
|
||||||
|
|
||||||
|
This would come as two doodads:
|
||||||
|
|
||||||
|
* The platform itself.
|
||||||
|
* A dotted-line outline of the platform to indicate where the platform will move
|
||||||
|
to when the level is played.
|
||||||
|
|
||||||
|
You will Link the platform to its destination outline to communicate which
|
||||||
|
destination is for which platform. In-game, the destination outline doodad will
|
||||||
|
be invisible and the platform(s) linked to it will move towards it, and then
|
||||||
|
move back to their original position, on a loop.
|
||||||
|
|
||||||
|
Implementation ideas:
|
||||||
|
|
||||||
|
* Add a concept of "rider/passenger" between doodads in a level.
|
||||||
|
* In the OnCollide() of the moving platform, if the player character is on top
|
||||||
|
of the platform, call "SetPassenger(e.Actor.ID())" to mark the player as
|
||||||
|
riding the platform.
|
||||||
|
* In OnLeave() call "RemovePassenger()" to un-mark the relationship.
|
||||||
|
* In the engine, if a moving actor has passengers, move the passengers along
|
||||||
|
with the actor.
|
||||||
|
|
||||||
|
# Completed Doodads
|
||||||
|
|
||||||
|
## Start Flag (DONE)
|
||||||
|
|
||||||
|
To control the player spawn point in a level, a "Start Flag" could be dragged
|
||||||
|
into the level. On level startup, the first Spawn Flag is located and the player
|
||||||
|
is put there.
|
||||||
|
|
||||||
|
If no Start Flag is found in the level, the player spawns at coordinate 0,0 at
|
||||||
|
the top-left corner of the page.
|
||||||
|
|
||||||
|
If multiple Start Flags are found, consider it an error and notify the user. The
|
||||||
|
player would appear at one of the flags randomly in this case.
|
||||||
|
|
||||||
|
## Crumbly Floor (DONE)
|
||||||
|
|
||||||
A rectangular floor piece with lines indicating cracks. Most similar to:
|
A rectangular floor piece with lines indicating cracks. Most similar to:
|
||||||
the break-away floors in Tomb Raider.
|
the break-away floors in Tomb Raider.
|
||||||
|
@ -20,3 +106,28 @@ Behavior:
|
||||||
* After a moment of rumbling, stop acting solid and play the break animation.
|
* After a moment of rumbling, stop acting solid and play the break animation.
|
||||||
A player standing on top of the floor falls through it now.
|
A player standing on top of the floor falls through it now.
|
||||||
* When the broken floor scrolls out of view it resets.
|
* When the broken floor scrolls out of view it resets.
|
||||||
|
|
||||||
|
## Two-state Blocks (DONE)
|
||||||
|
|
||||||
|
This is an idea how to add a global two-state ON/OFF set of doodads for levels,
|
||||||
|
similar to the two-state blocks in _Mario Maker 2_ or the blue switches
|
||||||
|
on _Chip's Challenge._
|
||||||
|
|
||||||
|
Currently, we have switches that can toggle doors open and closed but the map
|
||||||
|
editor must link these together manually. The two-state doodads instead should
|
||||||
|
work globally, where the ON/OFF switch should toggle the state of ALL two-state
|
||||||
|
doodads without needing to be manually linked up.
|
||||||
|
|
||||||
|
## Pub/Sub Broadcast
|
||||||
|
|
||||||
|
To implement the global messaging without manually linking the doodads, add a
|
||||||
|
new method to the `Message` API for the Doodad scripts:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Message.Broadcast("name", args...)
|
||||||
|
```
|
||||||
|
|
||||||
|
It will be like `Message.Publish()` but sends the message to ALL doodads whether
|
||||||
|
they're linked to the publisher or not.
|
||||||
|
|
||||||
|
On the recipient side, they will `Message.Subscribe("name", func)` as always.
|
||||||
|
|
|
@ -5,4 +5,7 @@ const (
|
||||||
AppName = "Project: Doodle"
|
AppName = "Project: Doodle"
|
||||||
Summary = "A drawing-based maze game"
|
Summary = "A drawing-based maze game"
|
||||||
Version = "0.0.10-alpha"
|
Version = "0.0.10-alpha"
|
||||||
|
|
||||||
|
// Update check URL
|
||||||
|
UpdateCheckJSON = "https://download.sketchymaze.com/version.json"
|
||||||
)
|
)
|
||||||
|
|
|
@ -106,7 +106,7 @@ func (d *Doodle) Run() error {
|
||||||
|
|
||||||
log.Info("Enter Main Loop")
|
log.Info("Enter Main Loop")
|
||||||
for d.running {
|
for d.running {
|
||||||
d.Engine.Clear(render.White)
|
// d.Engine.Clear(render.White)
|
||||||
|
|
||||||
start := time.Now() // Record how long this frame took.
|
start := time.Now() // Record how long this frame took.
|
||||||
shmem.Tick++
|
shmem.Tick++
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package doodle
|
package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||||
"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/native"
|
||||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/updater"
|
||||||
"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"
|
||||||
|
@ -15,11 +19,19 @@ import (
|
||||||
// MainScene implements the main menu of Doodle.
|
// MainScene implements the main menu of Doodle.
|
||||||
type MainScene struct {
|
type MainScene struct {
|
||||||
Supervisor *ui.Supervisor
|
Supervisor *ui.Supervisor
|
||||||
frame *ui.Frame
|
|
||||||
|
|
||||||
// Background wallpaper canvas.
|
// Background wallpaper canvas.
|
||||||
scripting *scripting.Supervisor
|
scripting *scripting.Supervisor
|
||||||
canvas *uix.Canvas
|
canvas *uix.Canvas
|
||||||
|
|
||||||
|
// UI components.
|
||||||
|
labelTitle *ui.Label
|
||||||
|
labelVersion *ui.Label
|
||||||
|
frame *ui.Frame // Main button frame
|
||||||
|
|
||||||
|
// Update check variables.
|
||||||
|
updateButton *ui.Button
|
||||||
|
updateInfo updater.VersionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name of the scene.
|
// Name of the scene.
|
||||||
|
@ -35,6 +47,51 @@ func (s *MainScene) Setup(d *Doodle) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Main title label
|
||||||
|
s.labelTitle = ui.NewLabel(ui.Label{
|
||||||
|
Text: branding.AppName,
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 46,
|
||||||
|
Color: render.Pink,
|
||||||
|
Stroke: render.SkyBlue,
|
||||||
|
Shadow: render.Black,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
s.labelTitle.Compute(d.Engine)
|
||||||
|
|
||||||
|
// Version label.
|
||||||
|
var shareware string
|
||||||
|
if balance.FreeVersion {
|
||||||
|
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
|
||||||
|
|
||||||
|
// "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(p render.Point) {
|
||||||
|
native.OpenURL(s.updateInfo.DownloadURL)
|
||||||
|
})
|
||||||
|
s.updateButton.Compute(d.Engine)
|
||||||
|
s.updateButton.Hide()
|
||||||
|
s.Supervisor.Add(s.updateButton)
|
||||||
|
|
||||||
// Main UI button frame.
|
// Main UI button frame.
|
||||||
frame := ui.NewFrame("frame")
|
frame := ui.NewFrame("frame")
|
||||||
s.frame = frame
|
s.frame = frame
|
||||||
|
@ -74,9 +131,26 @@ func (s *MainScene) Setup(d *Doodle) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for update in the background.
|
||||||
|
go s.checkUpdate()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkUpdate checks for a version update and shows the button.
|
||||||
|
func (s *MainScene) checkUpdate() {
|
||||||
|
info, err := updater.Check()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.LatestVersion != branding.Version {
|
||||||
|
s.updateInfo = info
|
||||||
|
s.updateButton.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetupDemoLevel configures the wallpaper behind the New screen,
|
// SetupDemoLevel configures the wallpaper behind the New screen,
|
||||||
// which demos a title screen demo level.
|
// which demos a title screen demo level.
|
||||||
func (s *MainScene) SetupDemoLevel(d *Doodle) error {
|
func (s *MainScene) SetupDemoLevel(d *Doodle) error {
|
||||||
|
@ -151,21 +225,26 @@ func (s *MainScene) Draw(d *Doodle) error {
|
||||||
H: d.height,
|
H: d.height,
|
||||||
})
|
})
|
||||||
|
|
||||||
label := ui.NewLabel(ui.Label{
|
// App title label.
|
||||||
Text: branding.AppName,
|
s.labelTitle.MoveTo(render.Point{
|
||||||
Font: render.Text{
|
X: (d.width / 2) - (s.labelTitle.Size().W / 2),
|
||||||
Size: 46,
|
|
||||||
Color: render.Pink,
|
|
||||||
Stroke: render.SkyBlue,
|
|
||||||
Shadow: render.Black,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
label.Compute(d.Engine)
|
|
||||||
label.MoveTo(render.Point{
|
|
||||||
X: (d.width / 2) - (label.Size().W / 2),
|
|
||||||
Y: 120,
|
Y: 120,
|
||||||
})
|
})
|
||||||
label.Present(d.Engine, label.Point())
|
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())
|
||||||
|
|
||||||
|
// 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.Compute(d.Engine)
|
||||||
s.frame.MoveTo(render.Point{
|
s.frame.MoveTo(render.Point{
|
||||||
|
|
|
@ -495,6 +495,18 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error {
|
||||||
// Loop the editor scene.
|
// Loop the editor scene.
|
||||||
func (s *MenuScene) Loop(d *Doodle, ev *event.State) error {
|
func (s *MenuScene) Loop(d *Doodle, ev *event.State) error {
|
||||||
s.Supervisor.Loop(ev)
|
s.Supervisor.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
64
pkg/native/browser.go
Normal file
64
pkg/native/browser.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 Windows this uses the ``start`` command.
|
||||||
|
// On MacOS this uses the ``open`` command.
|
||||||
|
func OpenURL(url string) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
go windowsOpenURL(url)
|
||||||
|
} else if runtime.GOOS == "linux" {
|
||||||
|
go linuxOpenURL(url)
|
||||||
|
} else if runtime.GOOS == "darwin" {
|
||||||
|
go macOpenURL(url)
|
||||||
|
} else {
|
||||||
|
log.Error("OpenURL: don't know how to open URLs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowsOpenURL(url string) {
|
||||||
|
_, err := exec.Command("start", url).Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("native.windowsOpenURL(%s): %s", url, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func macOpenURL(url string) {
|
||||||
|
_, err := exec.Command("open", url).Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("native.macOpenURL(%s): %s", url, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func linuxOpenURL(url string) {
|
||||||
|
// Commands to look for.
|
||||||
|
var commands = []string{
|
||||||
|
"xdg-open",
|
||||||
|
"firefox",
|
||||||
|
"google-chrome",
|
||||||
|
"chromium-browser",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, command := range commands {
|
||||||
|
log.Debug("OpenURL(linux): try %s %s", command, url)
|
||||||
|
_, err := exec.Command(command, url).Output()
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error(
|
||||||
|
"native.linuxOpenURL(%s): could not find browser executable, tried %+v",
|
||||||
|
url, commands,
|
||||||
|
)
|
||||||
|
}
|
10
pkg/native/browser_wasm.go
Normal file
10
pkg/native/browser_wasm.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package native
|
||||||
|
|
||||||
|
import "syscall/js"
|
||||||
|
|
||||||
|
// OpenURL opens a new window to the given URL, for WASM environment.
|
||||||
|
func OpenURL(url string) {
|
||||||
|
js.Global().Get("window").Call("open", url)
|
||||||
|
}
|
4
pkg/native/doc.go
Normal file
4
pkg/native/doc.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// Package native provides native system functions for Linux, MacOS and
|
||||||
|
// Windows to perform operating system-specific tasks such as open web
|
||||||
|
// links or native dialog boxes.
|
||||||
|
package native
|
63
pkg/updater/updater.go
Normal file
63
pkg/updater/updater.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// Package updater checks for updates to Doodle.
|
||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VersionInfo holds the version.json data for self-update check.
|
||||||
|
type VersionInfo struct {
|
||||||
|
LatestVersion string `json:"latestVersion"`
|
||||||
|
DownloadURL string `json:"downloadUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last result of the update check, until forced to re-check.
|
||||||
|
var lastUpdate VersionInfo
|
||||||
|
|
||||||
|
// Check for new updates.
|
||||||
|
func Check() (VersionInfo, error) {
|
||||||
|
var result VersionInfo
|
||||||
|
|
||||||
|
// Return last cached check.
|
||||||
|
if lastUpdate.LatestVersion != "" {
|
||||||
|
return lastUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Checking for app updates")
|
||||||
|
|
||||||
|
resp, err := client.Get(branding.UpdateCheckJSON)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("updater.Check: HTTP error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return result, fmt.Errorf("updater.Check: unexpected HTTP status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON response.
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("updater.Check: JSON parse error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdate = result
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckNow forces a re-check of the update info.
|
||||||
|
func CheckNow() (VersionInfo, error) {
|
||||||
|
lastUpdate = VersionInfo{}
|
||||||
|
return Check()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user