2017-10-27 01:03:11 +00:00
|
|
|
package doodle
|
|
|
|
|
2017-10-27 02:26:54 +00:00
|
|
|
import (
|
2018-09-26 17:04:46 +00:00
|
|
|
"fmt"
|
2019-07-02 22:24:46 +00:00
|
|
|
"path/filepath"
|
2021-09-11 23:52:22 +00:00
|
|
|
"strconv"
|
2018-09-26 17:04:46 +00:00
|
|
|
"strings"
|
2018-06-17 02:59:23 +00:00
|
|
|
"time"
|
2017-10-27 02:26:54 +00:00
|
|
|
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
2019-06-25 21:57:11 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
2022-02-20 02:25:36 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
2020-11-18 02:22:48 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
2021-12-27 04:48:29 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
2020-11-16 02:02:35 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/modal"
|
2021-07-19 03:04:24 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
|
2020-06-05 04:55:54 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/native"
|
2021-06-10 05:36:32 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/pattern"
|
2019-06-27 18:57:26 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
2021-06-20 05:14:41 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/usercfg"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/windows"
|
2019-12-23 02:34:31 +00:00
|
|
|
golog "git.kirsle.net/go/log"
|
|
|
|
"git.kirsle.net/go/render"
|
|
|
|
"git.kirsle.net/go/render/event"
|
2021-06-20 05:14:41 +00:00
|
|
|
"git.kirsle.net/go/ui"
|
2017-10-27 02:26:54 +00:00
|
|
|
)
|
2017-10-27 01:03:11 +00:00
|
|
|
|
2018-06-17 02:59:23 +00:00
|
|
|
const (
|
|
|
|
// TargetFPS is the frame rate to cap the game to.
|
2018-06-17 14:56:51 +00:00
|
|
|
TargetFPS = 1000 / 60 // 60 FPS
|
2018-06-17 02:59:23 +00:00
|
|
|
|
|
|
|
// Millisecond64 is a time.Millisecond casted to float64.
|
|
|
|
Millisecond64 = float64(time.Millisecond)
|
|
|
|
)
|
2017-10-27 01:03:11 +00:00
|
|
|
|
|
|
|
// Doodle is the game object.
|
|
|
|
type Doodle struct {
|
2018-08-11 00:19:47 +00:00
|
|
|
Debug bool
|
|
|
|
Engine render.Engine
|
|
|
|
engineReady bool
|
2017-10-27 01:03:11 +00:00
|
|
|
|
2018-10-19 20:31:58 +00:00
|
|
|
// Easy access to the event state, for the debug overlay to use.
|
|
|
|
// Might not be thread safe.
|
2019-12-22 22:11:01 +00:00
|
|
|
event *event.State
|
2018-10-19 20:31:58 +00:00
|
|
|
|
2018-06-17 02:59:23 +00:00
|
|
|
startTime time.Time
|
|
|
|
running bool
|
2018-10-19 20:31:58 +00:00
|
|
|
width int
|
|
|
|
height int
|
2018-06-17 02:59:23 +00:00
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// Command line shell options.
|
|
|
|
shell Shell
|
|
|
|
|
2018-07-26 02:38:54 +00:00
|
|
|
Scene Scene
|
2017-10-27 01:03:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New initializes the game object.
|
2018-07-22 00:12:22 +00:00
|
|
|
func New(debug bool, engine render.Engine) *Doodle {
|
2017-10-27 01:03:11 +00:00
|
|
|
d := &Doodle{
|
2018-06-17 02:59:23 +00:00
|
|
|
Debug: debug,
|
2018-07-22 00:12:22 +00:00
|
|
|
Engine: engine,
|
2018-06-17 02:59:23 +00:00
|
|
|
startTime: time.Now(),
|
|
|
|
running: true,
|
2018-10-19 20:31:58 +00:00
|
|
|
width: balance.Width,
|
|
|
|
height: balance.Height,
|
2018-06-17 02:59:23 +00:00
|
|
|
}
|
2018-07-22 03:43:01 +00:00
|
|
|
d.shell = NewShell(d)
|
2018-06-17 02:59:23 +00:00
|
|
|
|
2019-06-27 18:57:26 +00:00
|
|
|
// Make the render engine globally available. TODO: for wasm/ToBitmap
|
|
|
|
shmem.CurrentRenderEngine = engine
|
|
|
|
shmem.Flash = d.Flash
|
2021-10-11 22:57:33 +00:00
|
|
|
shmem.FlashError = d.FlashError
|
2020-07-10 02:38:37 +00:00
|
|
|
shmem.Prompt = d.Prompt
|
2019-06-27 18:57:26 +00:00
|
|
|
|
2019-04-16 06:07:15 +00:00
|
|
|
if debug {
|
|
|
|
log.Logger.Config.Level = golog.DebugLevel
|
2019-06-25 21:57:11 +00:00
|
|
|
// DebugOverlay = true // on by default in debug mode, F3 to disable
|
2017-10-27 01:03:11 +00:00
|
|
|
}
|
2018-06-17 02:59:23 +00:00
|
|
|
|
2017-10-27 02:26:54 +00:00
|
|
|
return d
|
|
|
|
}
|
2017-10-27 01:03:11 +00:00
|
|
|
|
2019-06-27 01:36:54 +00:00
|
|
|
// SetWindowSize sets the size of the Doodle window.
|
|
|
|
func (d *Doodle) SetWindowSize(width, height int) {
|
|
|
|
d.width = width
|
|
|
|
d.height = height
|
|
|
|
}
|
|
|
|
|
2019-06-25 21:57:11 +00:00
|
|
|
// Title returns the game's preferred window title.
|
|
|
|
func (d *Doodle) Title() string {
|
|
|
|
return fmt.Sprintf("%s v%s", branding.AppName, branding.Version)
|
|
|
|
}
|
|
|
|
|
2018-08-11 00:19:47 +00:00
|
|
|
// SetupEngine sets up the rendering engine.
|
|
|
|
func (d *Doodle) SetupEngine() error {
|
2020-11-16 02:02:35 +00:00
|
|
|
// Set up the rendering engine (SDL2, etc.)
|
2018-07-22 00:12:22 +00:00
|
|
|
if err := d.Engine.Setup(); err != nil {
|
2017-10-27 02:26:54 +00:00
|
|
|
return err
|
2017-10-27 01:03:11 +00:00
|
|
|
}
|
2018-08-11 00:19:47 +00:00
|
|
|
d.engineReady = true
|
2020-11-16 02:02:35 +00:00
|
|
|
|
|
|
|
// Initialize the UI modal manager.
|
|
|
|
modal.Initialize(d.Engine)
|
2021-06-10 05:36:32 +00:00
|
|
|
|
|
|
|
// Preload the builtin brush patterns.
|
|
|
|
pattern.LoadBuiltins(d.Engine)
|
2020-11-16 02:02:35 +00:00
|
|
|
|
2018-08-11 00:19:47 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run initializes SDL and starts the main loop.
|
|
|
|
func (d *Doodle) Run() error {
|
|
|
|
if !d.engineReady {
|
|
|
|
if err := d.SetupEngine(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-10-27 02:26:54 +00:00
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
// Set up the default scene.
|
2018-07-26 02:38:54 +00:00
|
|
|
if d.Scene == nil {
|
2019-06-25 21:57:11 +00:00
|
|
|
d.Goto(&MainScene{})
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
|
|
|
|
2018-06-17 02:59:23 +00:00
|
|
|
log.Info("Enter Main Loop")
|
2017-10-27 02:26:54 +00:00
|
|
|
for d.running {
|
2020-01-02 01:50:15 +00:00
|
|
|
// d.Engine.Clear(render.White)
|
2018-07-22 03:43:01 +00:00
|
|
|
|
2018-07-22 00:12:22 +00:00
|
|
|
start := time.Now() // Record how long this frame took.
|
2019-07-05 23:04:36 +00:00
|
|
|
shmem.Tick++
|
2018-06-17 14:56:51 +00:00
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
// Poll for events.
|
2018-07-22 00:12:22 +00:00
|
|
|
ev, err := d.Engine.Poll()
|
2018-06-21 01:43:14 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("event poll error: %s", err)
|
2018-07-22 00:12:22 +00:00
|
|
|
d.running = false
|
|
|
|
break
|
|
|
|
}
|
2018-10-19 20:31:58 +00:00
|
|
|
d.event = ev
|
2018-07-22 00:12:22 +00:00
|
|
|
|
2022-02-20 02:25:36 +00:00
|
|
|
// Let the gamepad controller check for events, if it's in MouseMode
|
|
|
|
// it will fake the mouse cursor.
|
|
|
|
gamepad.Loop(ev)
|
|
|
|
|
|
|
|
// Globally store the cursor position.
|
|
|
|
shmem.Cursor = render.NewPoint(ev.CursorX, ev.CursorY)
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// Command line shell.
|
|
|
|
if d.shell.Open {
|
WIP Publish Dialog + UI Improvements
* File->Publish Level in the Level Editor opens the Publish window,
where you can embed custom doodads into your level and export a
portable .level file you can share with others.
* Currently does not actually export a level file yet.
* The dialog lists all unique doodad names in use in your level, and
designates which are built-ins and which are custom (paginated).
* A checkbox would let the user embed built-in doodads into their level,
as well, locking it in to those versions and not using updated
versions from future game releases.
UI Improvements:
* Added styling for a "Primary" UI button, rendered in deep blue.
* Pop-up modals (Alert, Confirm) color their Ok button as Primary.
* The Enter key pressed during an Alert or Confirm modal will invoke its
default button and close the modal, corresponding to its Primary
button.
* The developer console is now opened with the tilde/grave key ` instead
of the Enter key, so that the Enter key is free to click through
modals.
* In the "Open/Edit Drawing" window, a "Browse..." button is added to
the level and doodad sections, spawning a native File Open dialog to
pick a .level or .doodad outside the config root.
2021-06-11 05:31:30 +00:00
|
|
|
} else if keybind.ShellKey(ev) {
|
2018-07-22 03:43:01 +00:00
|
|
|
log.Debug("Shell: opening shell")
|
|
|
|
d.shell.Open = true
|
|
|
|
} else {
|
2020-11-18 02:22:48 +00:00
|
|
|
if keybind.Help(ev) {
|
2020-11-21 07:35:37 +00:00
|
|
|
// Launch the local guidebook
|
|
|
|
native.OpenLocalURL(balance.GuidebookPath)
|
2020-11-18 02:22:48 +00:00
|
|
|
} else if keybind.DebugOverlay(ev) {
|
2019-04-19 23:21:04 +00:00
|
|
|
DebugOverlay = !DebugOverlay
|
2020-11-18 02:22:48 +00:00
|
|
|
} else if keybind.DebugCollision(ev) {
|
2019-04-19 23:21:04 +00:00
|
|
|
DebugCollision = !DebugCollision
|
|
|
|
}
|
|
|
|
|
2021-07-19 03:04:24 +00:00
|
|
|
// Make sure no UI modals (alerts, confirms)
|
|
|
|
// or loadscreen are currently visible.
|
|
|
|
if !modal.Handled(ev) {
|
2022-02-21 21:09:51 +00:00
|
|
|
// Global event handlers.
|
|
|
|
if keybind.Shutdown(ev) {
|
|
|
|
if d.Debug { // fast exit in -debug mode.
|
|
|
|
d.running = false
|
|
|
|
} else {
|
|
|
|
d.ConfirmExit()
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-11-16 02:02:35 +00:00
|
|
|
// Run the scene's logic.
|
|
|
|
err = d.Scene.Loop(d, ev)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-07-22 03:43:01 +00:00
|
|
|
}
|
2020-11-16 02:02:35 +00:00
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// Draw the scene.
|
2018-07-26 02:38:54 +00:00
|
|
|
d.Scene.Draw(d)
|
2018-07-22 03:43:01 +00:00
|
|
|
|
2021-07-19 03:04:24 +00:00
|
|
|
// Draw the loadscreen if it is active.
|
|
|
|
loadscreen.Loop(render.NewRect(d.width, d.height), d.Engine)
|
|
|
|
|
2020-11-16 02:02:35 +00:00
|
|
|
// Draw modals on top of the game UI.
|
|
|
|
modal.Draw()
|
|
|
|
|
|
|
|
// Draw the shell, always on top of UI and modals.
|
2018-07-22 03:43:01 +00:00
|
|
|
err = d.shell.Draw(d, ev)
|
2018-06-17 14:56:51 +00:00
|
|
|
if err != nil {
|
2018-07-22 03:43:01 +00:00
|
|
|
log.Error("shell error: %s", err)
|
|
|
|
d.running = false
|
|
|
|
break
|
2018-06-17 14:56:51 +00:00
|
|
|
}
|
|
|
|
|
2018-07-22 00:12:22 +00:00
|
|
|
// Draw the debug overlay over all scenes.
|
2018-07-26 02:38:54 +00:00
|
|
|
d.DrawDebugOverlay()
|
2018-07-22 00:12:22 +00:00
|
|
|
|
2022-02-20 02:25:36 +00:00
|
|
|
// Let the gamepad controller draw in case of MouseMode to show the cursor.
|
|
|
|
gamepad.Draw(d.Engine)
|
|
|
|
|
2018-07-22 00:12:22 +00:00
|
|
|
// Render the pixels to the screen.
|
2018-07-22 03:43:01 +00:00
|
|
|
err = d.Engine.Present()
|
2018-07-22 00:12:22 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("draw error: %s", err)
|
|
|
|
d.running = false
|
|
|
|
break
|
|
|
|
}
|
2018-06-17 02:59:23 +00:00
|
|
|
|
2018-06-17 14:56:51 +00:00
|
|
|
// Delay to maintain the target frames per second.
|
|
|
|
var delay uint32
|
2019-04-10 01:28:08 +00:00
|
|
|
if !fpsDoNotCap {
|
|
|
|
elapsed := time.Now().Sub(start)
|
|
|
|
tmp := elapsed / time.Millisecond
|
|
|
|
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under
|
|
|
|
delay = uint32(TargetFPS - int(tmp))
|
|
|
|
}
|
|
|
|
d.Engine.Delay(delay)
|
2018-06-17 14:56:51 +00:00
|
|
|
}
|
2018-06-17 02:59:23 +00:00
|
|
|
|
2018-06-17 14:56:51 +00:00
|
|
|
// Track how long this frame took to measure FPS over time.
|
2018-06-17 02:59:23 +00:00
|
|
|
d.TrackFPS(delay)
|
2018-07-22 03:43:01 +00:00
|
|
|
|
|
|
|
// Consume any lingering key sym.
|
2021-06-20 05:14:41 +00:00
|
|
|
// ev.ResetKeyDown()
|
2017-10-27 02:26:54 +00:00
|
|
|
}
|
|
|
|
|
2018-06-17 02:59:23 +00:00
|
|
|
log.Warn("Main Loop Exited! Shutting down...")
|
2017-10-27 02:26:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-06-20 05:14:41 +00:00
|
|
|
// MakeSettingsWindow initializes the windows/settings.go window
|
|
|
|
// from anywhere you need it, binding all the variables in.
|
|
|
|
func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window {
|
|
|
|
cfg := windows.Settings{
|
|
|
|
Supervisor: supervisor,
|
|
|
|
Engine: d.Engine,
|
|
|
|
SceneName: d.Scene.Name(),
|
|
|
|
OnApply: func() {
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
// Boolean checkbox bindings
|
|
|
|
DebugOverlay: &DebugOverlay,
|
|
|
|
DebugCollision: &DebugCollision,
|
|
|
|
HorizontalToolbars: &usercfg.Current.HorizontalToolbars,
|
2021-09-12 04:18:22 +00:00
|
|
|
EnableFeatures: &usercfg.Current.EnableFeatures,
|
2021-10-11 22:57:33 +00:00
|
|
|
CrosshairSize: &usercfg.Current.CrosshairSize,
|
|
|
|
CrosshairColor: &usercfg.Current.CrosshairColor,
|
2021-10-13 03:49:48 +00:00
|
|
|
HideTouchHints: &usercfg.Current.HideTouchHints,
|
2022-01-03 06:36:32 +00:00
|
|
|
DisableAutosave: &usercfg.Current.DisableAutosave,
|
2022-02-20 02:25:36 +00:00
|
|
|
ControllerStyle: &usercfg.Current.ControllerStyle,
|
2021-06-20 05:14:41 +00:00
|
|
|
}
|
|
|
|
return windows.MakeSettingsWindow(d.width, d.height, cfg)
|
|
|
|
}
|
|
|
|
|
2020-11-16 02:02:35 +00:00
|
|
|
// ConfirmExit may shut down Doodle gracefully after showing the user a
|
|
|
|
// confirmation modal.
|
|
|
|
func (d *Doodle) ConfirmExit() {
|
|
|
|
modal.Confirm("Are you sure you want to quit %s?", branding.AppName).
|
|
|
|
WithTitle("Confirm Quit").Then(func() {
|
|
|
|
d.running = false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// NewMap loads a new map in Edit Mode.
|
|
|
|
func (d *Doodle) NewMap() {
|
|
|
|
log.Info("Starting a new map")
|
|
|
|
scene := &EditorScene{}
|
|
|
|
d.Goto(scene)
|
|
|
|
}
|
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
// NewDoodad loads a new Doodad in Edit Mode.
|
2021-09-11 23:52:22 +00:00
|
|
|
// If size is zero, it prompts the user to select a size or accept the default size.
|
2018-09-26 17:04:46 +00:00
|
|
|
func (d *Doodle) NewDoodad(size int) {
|
2021-09-11 23:52:22 +00:00
|
|
|
if size == 0 {
|
|
|
|
d.Prompt(fmt.Sprintf("Doodad size or %d>", balance.DoodadSize), func(answer string) {
|
|
|
|
size := balance.DoodadSize
|
|
|
|
if answer != "" {
|
|
|
|
i, err := strconv.Atoi(answer)
|
|
|
|
if err != nil {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("Error: Doodad size must be a number.")
|
2021-09-11 23:52:22 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
size = i
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recurse with the proper answer.
|
|
|
|
if size <= 0 {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("Error: Doodad size must be a positive number.")
|
2021-09-11 23:52:22 +00:00
|
|
|
}
|
|
|
|
d.NewDoodad(size)
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
log.Info("Starting a new doodad")
|
|
|
|
scene := &EditorScene{
|
|
|
|
DrawingType: enum.DoodadDrawing,
|
|
|
|
DoodadSize: size,
|
|
|
|
}
|
|
|
|
d.Goto(scene)
|
|
|
|
}
|
|
|
|
|
|
|
|
// EditDrawing loads a drawing (Level or Doodad) in Edit Mode.
|
|
|
|
func (d *Doodle) EditDrawing(filename string) error {
|
|
|
|
log.Info("Loading drawing from file: %s", filename)
|
2019-07-02 22:24:46 +00:00
|
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
2018-09-26 17:04:46 +00:00
|
|
|
|
2018-08-11 00:19:47 +00:00
|
|
|
scene := &EditorScene{
|
|
|
|
Filename: filename,
|
|
|
|
OpenFile: true,
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
2018-09-26 17:04:46 +00:00
|
|
|
|
|
|
|
switch ext {
|
2019-07-02 22:24:46 +00:00
|
|
|
case ".level":
|
|
|
|
case ".map":
|
2019-04-10 00:35:44 +00:00
|
|
|
log.Info("is a Level type")
|
2018-09-26 17:04:46 +00:00
|
|
|
scene.DrawingType = enum.LevelDrawing
|
2019-07-02 22:24:46 +00:00
|
|
|
case ".doodad":
|
2018-09-26 17:04:46 +00:00
|
|
|
scene.DrawingType = enum.DoodadDrawing
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("file extension '%s' doesn't indicate its drawing type", ext)
|
|
|
|
}
|
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
d.Goto(scene)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-21 02:00:46 +00:00
|
|
|
// PlayLevel loads a map from JSON into the PlayScene.
|
|
|
|
func (d *Doodle) PlayLevel(filename string) error {
|
|
|
|
log.Info("Loading level from file: %s", filename)
|
2018-07-24 03:10:53 +00:00
|
|
|
scene := &PlayScene{
|
|
|
|
Filename: filename,
|
2018-06-21 02:00:46 +00:00
|
|
|
}
|
|
|
|
d.Goto(scene)
|
|
|
|
return nil
|
|
|
|
}
|
2021-12-27 04:48:29 +00:00
|
|
|
|
|
|
|
// PlayFromLevelpack initializes the Play Scene from a level as part of
|
|
|
|
// a levelpack.
|
|
|
|
func (d *Doodle) PlayFromLevelpack(pack levelpack.LevelPack, which levelpack.Level) error {
|
|
|
|
log.Info("Loading level %s from levelpack %s", which.Filename, pack.Title)
|
|
|
|
scene := &PlayScene{
|
|
|
|
Filename: which.Filename,
|
|
|
|
LevelPack: &pack,
|
|
|
|
}
|
|
|
|
d.Goto(scene)
|
|
|
|
return nil
|
|
|
|
}
|