Add app version/update check to the Main Scene

This commit is contained in:
Noah 2020-01-01 17:50:15 -08:00
parent 8965a7d86a
commit cd31868a13
9 changed files with 362 additions and 16 deletions

View File

@ -1,6 +1,92 @@
# 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:
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.
A player standing on top of the floor falls through it now.
* 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.

View File

@ -5,4 +5,7 @@ const (
AppName = "Project: Doodle"
Summary = "A drawing-based maze game"
Version = "0.0.10-alpha"
// Update check URL
UpdateCheckJSON = "https://download.sketchymaze.com/version.json"
)

View File

@ -106,7 +106,7 @@ func (d *Doodle) Run() error {
log.Info("Enter Main Loop")
for d.running {
d.Engine.Clear(render.White)
// d.Engine.Clear(render.White)
start := time.Now() // Record how long this frame took.
shmem.Tick++

View File

@ -1,12 +1,16 @@
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/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/apps/doodle/pkg/updater"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
@ -15,11 +19,19 @@ import (
// MainScene implements the main menu of Doodle.
type MainScene struct {
Supervisor *ui.Supervisor
frame *ui.Frame
// Background wallpaper canvas.
scripting *scripting.Supervisor
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.
@ -35,6 +47,51 @@ func (s *MainScene) Setup(d *Doodle) error {
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.
frame := ui.NewFrame("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
}
// 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,
// which demos a title screen demo level.
func (s *MainScene) SetupDemoLevel(d *Doodle) error {
@ -151,21 +225,26 @@ func (s *MainScene) Draw(d *Doodle) error {
H: d.height,
})
label := ui.NewLabel(ui.Label{
Text: branding.AppName,
Font: render.Text{
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),
// App title label.
s.labelTitle.MoveTo(render.Point{
X: (d.width / 2) - (s.labelTitle.Size().W / 2),
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.MoveTo(render.Point{

View File

@ -495,6 +495,18 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error {
// Loop the editor scene.
func (s *MenuScene) Loop(d *Doodle, ev *event.State) error {
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
}

64
pkg/native/browser.go Normal file
View 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,
)
}

View 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
View 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
View 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()
}