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
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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++
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
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