From cd31868a135b3f2ff17ae86541ed5e0c8ae39219 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 1 Jan 2020 17:50:15 -0800 Subject: [PATCH] Add app version/update check to the Main Scene --- docs/Doodad Ideas.md | 113 ++++++++++++++++++++++++++++++++++++- pkg/branding/branding.go | 3 + pkg/doodle.go | 2 +- pkg/main_scene.go | 107 ++++++++++++++++++++++++++++++----- pkg/menu_scene.go | 12 ++++ pkg/native/browser.go | 64 +++++++++++++++++++++ pkg/native/browser_wasm.go | 10 ++++ pkg/native/doc.go | 4 ++ pkg/updater/updater.go | 63 +++++++++++++++++++++ 9 files changed, 362 insertions(+), 16 deletions(-) create mode 100644 pkg/native/browser.go create mode 100644 pkg/native/browser_wasm.go create mode 100644 pkg/native/doc.go create mode 100644 pkg/updater/updater.go diff --git a/docs/Doodad Ideas.md b/docs/Doodad Ideas.md index b78d517..5bf6368 100644 --- a/docs/Doodad Ideas.md +++ b/docs/Doodad Ideas.md @@ -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. diff --git a/pkg/branding/branding.go b/pkg/branding/branding.go index 5c3b015..5164069 100644 --- a/pkg/branding/branding.go +++ b/pkg/branding/branding.go @@ -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" ) diff --git a/pkg/doodle.go b/pkg/doodle.go index 46cd030..7032a5e 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -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++ diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 607c4b8..30ad72c 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -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{ diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index 151b1a3..7842f95 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -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 } diff --git a/pkg/native/browser.go b/pkg/native/browser.go new file mode 100644 index 0000000..4ed4339 --- /dev/null +++ b/pkg/native/browser.go @@ -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, + ) +} diff --git a/pkg/native/browser_wasm.go b/pkg/native/browser_wasm.go new file mode 100644 index 0000000..f723973 --- /dev/null +++ b/pkg/native/browser_wasm.go @@ -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) +} diff --git a/pkg/native/doc.go b/pkg/native/doc.go new file mode 100644 index 0000000..b45916a --- /dev/null +++ b/pkg/native/doc.go @@ -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 diff --git a/pkg/updater/updater.go b/pkg/updater/updater.go new file mode 100644 index 0000000..298deb6 --- /dev/null +++ b/pkg/updater/updater.go @@ -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() +}