Noah Petherbridge
ddcad27485
Convert the Chunker size to a uint8 so chunk sizes are limited to 255px. This means that inside of a chunk, uint8's can track the relative pixel coordinates and result in a great memory savings since all of these uint8's are currently 64-bits wide apiece. WIP on rectangular shaped doodads: * You can create such a doodad in the editor and draw it normally. * It doesn't draw the right size when dragged into your level however: - In uix.Actor.Size() it gets a rect of the doodad's square Chunker size, instead of getting the proper doodad.Size rect. - If you give it the doodad.Size rect, it draws the Canvas size correctly instead of a square - the full drawing appears and in gameplay its hitbox (assuming the same large rectangle size) works correctly in-game. - But, the doodad has scrolling issues when it gets to the top or left edge of the screen! This old gnarly bug has come back. For some reason square canvas doodads draw correctly but rectangular ones have the drawing scroll just a bit - how far it scrolls is proportional to how big the doodad is, with the Start Flag only scrolling a few pixels before it stops.
353 lines
8.6 KiB
Go
353 lines
8.6 KiB
Go
package doodle
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/cursor"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/gamepad"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/pattern"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/usercfg"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/windows"
|
|
golog "git.kirsle.net/go/log"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/render/event"
|
|
"git.kirsle.net/go/ui"
|
|
)
|
|
|
|
const (
|
|
// TargetFPS is the frame rate to cap the game to.
|
|
TargetFPS = 1000 / 60 // 60 FPS
|
|
|
|
// Millisecond64 is a time.Millisecond casted to float64.
|
|
Millisecond64 = float64(time.Millisecond)
|
|
)
|
|
|
|
// Doodle is the game object.
|
|
type Doodle struct {
|
|
Debug bool
|
|
Engine render.Engine
|
|
engineReady bool
|
|
|
|
// Easy access to the event state, for the debug overlay to use.
|
|
// Might not be thread safe.
|
|
event *event.State
|
|
|
|
startTime time.Time
|
|
running bool
|
|
width int
|
|
height int
|
|
|
|
// Command line shell options.
|
|
shell Shell
|
|
|
|
Scene Scene
|
|
}
|
|
|
|
// New initializes the game object.
|
|
func New(debug bool, engine render.Engine) *Doodle {
|
|
d := &Doodle{
|
|
Debug: debug,
|
|
Engine: engine,
|
|
startTime: time.Now(),
|
|
running: true,
|
|
width: balance.Width,
|
|
height: balance.Height,
|
|
}
|
|
d.shell = NewShell(d)
|
|
|
|
// Make the render engine globally available. TODO: for wasm/ToBitmap
|
|
shmem.CurrentRenderEngine = engine
|
|
shmem.Flash = d.Flash
|
|
shmem.FlashError = d.FlashError
|
|
shmem.Prompt = d.Prompt
|
|
shmem.PromptPre = d.PromptPre
|
|
|
|
if debug {
|
|
log.Logger.Config.Level = golog.DebugLevel
|
|
// DebugOverlay = true // on by default in debug mode, F3 to disable
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// SetWindowSize sets the size of the Doodle window.
|
|
func (d *Doodle) SetWindowSize(width, height int) {
|
|
d.width = width
|
|
d.height = height
|
|
}
|
|
|
|
// Title returns the game's preferred window title.
|
|
func (d *Doodle) Title() string {
|
|
return fmt.Sprintf("%s v%s", branding.AppName, branding.Version)
|
|
}
|
|
|
|
// SetupEngine sets up the rendering engine.
|
|
func (d *Doodle) SetupEngine() error {
|
|
// Set up the rendering engine (SDL2, etc.)
|
|
if err := d.Engine.Setup(); err != nil {
|
|
return err
|
|
}
|
|
d.engineReady = true
|
|
|
|
// Initialize the UI modal manager.
|
|
modal.Initialize(d.Engine)
|
|
|
|
// Preload the builtin brush patterns.
|
|
pattern.LoadBuiltins(d.Engine)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Set up the default scene.
|
|
if d.Scene == nil {
|
|
d.Goto(&MainScene{})
|
|
}
|
|
|
|
log.Info("Enter Main Loop")
|
|
for d.running {
|
|
// d.Engine.Clear(render.White)
|
|
|
|
start := time.Now() // Record how long this frame took.
|
|
shmem.Tick++
|
|
|
|
// Poll for events.
|
|
ev, err := d.Engine.Poll()
|
|
if err != nil {
|
|
log.Error("event poll error: %s", err)
|
|
d.running = false
|
|
break
|
|
}
|
|
d.event = ev
|
|
|
|
// Always have an accurate idea of the window size.
|
|
if ev.WindowResized {
|
|
d.width, d.height = d.Engine.WindowSize()
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Command line shell.
|
|
if d.shell.Open {
|
|
} else if keybind.ShellKey(ev) {
|
|
log.Debug("Shell: opening shell")
|
|
d.shell.Open = true
|
|
} else {
|
|
if keybind.Help(ev) {
|
|
// Launch the local guidebook
|
|
native.OpenLocalURL(balance.GuidebookPath)
|
|
} else if keybind.DebugOverlay(ev) {
|
|
DebugOverlay = !DebugOverlay
|
|
} else if keybind.DebugCollision(ev) {
|
|
DebugCollision = !DebugCollision
|
|
}
|
|
|
|
// Make sure no UI modals (alerts, confirms)
|
|
// or loadscreen are currently visible.
|
|
if !modal.Handled(ev) && !exceptions.Handled(ev) {
|
|
// Global event handlers.
|
|
if keybind.Shutdown(ev) {
|
|
if d.Debug { // fast exit in -debug mode.
|
|
d.running = false
|
|
} else {
|
|
d.ConfirmExit()
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Run the scene's logic.
|
|
err = d.Scene.Loop(d, ev)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Draw the scene.
|
|
d.Scene.Draw(d)
|
|
|
|
// Draw the loadscreen if it is active.
|
|
loadscreen.Loop(render.NewRect(d.width, d.height), d.Engine)
|
|
|
|
// Draw modals on top of the game UI.
|
|
exceptions.Draw(d.Engine)
|
|
modal.Draw()
|
|
|
|
// Draw the shell, always on top of UI and modals.
|
|
err = d.shell.Draw(d, ev)
|
|
if err != nil {
|
|
log.Error("shell error: %s", err)
|
|
d.running = false
|
|
break
|
|
}
|
|
|
|
// Draw the debug overlay over all scenes.
|
|
d.DrawDebugOverlay()
|
|
|
|
// Draw our custom mouse cursor to the screen.
|
|
cursor.Draw(d.Engine)
|
|
|
|
// Render the pixels to the screen.
|
|
err = d.Engine.Present()
|
|
if err != nil {
|
|
log.Error("draw error: %s", err)
|
|
d.running = false
|
|
break
|
|
}
|
|
|
|
// Delay to maintain the target frames per second.
|
|
var delay uint32
|
|
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)
|
|
}
|
|
|
|
// Track how long this frame took to measure FPS over time.
|
|
d.TrackFPS(delay)
|
|
|
|
// Consume any lingering key sym.
|
|
// ev.ResetKeyDown()
|
|
}
|
|
|
|
log.Warn("Main Loop Exited! Shutting down...")
|
|
return nil
|
|
}
|
|
|
|
// 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() {
|
|
|
|
},
|
|
OnOpenCheatsWindow: func() *ui.Window {
|
|
return d.MakeCheatsWindow(supervisor)
|
|
},
|
|
|
|
// Boolean checkbox bindings
|
|
DebugOverlay: &DebugOverlay,
|
|
DebugCollision: &DebugCollision,
|
|
HorizontalToolbars: &usercfg.Current.HorizontalToolbars,
|
|
EnableFeatures: &usercfg.Current.EnableFeatures,
|
|
CrosshairSize: &usercfg.Current.CrosshairSize,
|
|
CrosshairColor: &usercfg.Current.CrosshairColor,
|
|
HideTouchHints: &usercfg.Current.HideTouchHints,
|
|
DisableAutosave: &usercfg.Current.DisableAutosave,
|
|
ControllerStyle: &usercfg.Current.ControllerStyle,
|
|
}
|
|
return windows.MakeSettingsWindow(d.width, d.height, cfg)
|
|
}
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
// NewMap loads a new map in Edit Mode.
|
|
func (d *Doodle) NewMap() {
|
|
log.Info("Starting a new map")
|
|
scene := &EditorScene{}
|
|
d.Goto(scene)
|
|
}
|
|
|
|
// NewDoodad loads a new Doodad in Edit Mode.
|
|
// If size is zero, it prompts the user to select a size or accept the default size.
|
|
func (d *Doodle) NewDoodad(width, height int) {
|
|
if width+height == 0 {
|
|
width = balance.DoodadSize
|
|
height = width
|
|
}
|
|
|
|
log.Info("Starting a new doodad")
|
|
scene := &EditorScene{
|
|
DrawingType: enum.DoodadDrawing,
|
|
DoodadSize: render.NewRect(width, height),
|
|
}
|
|
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)
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
|
|
scene := &EditorScene{
|
|
Filename: filename,
|
|
OpenFile: true,
|
|
}
|
|
|
|
switch ext {
|
|
case ".level":
|
|
case ".map":
|
|
log.Info("is a Level type")
|
|
scene.DrawingType = enum.LevelDrawing
|
|
case ".doodad":
|
|
scene.DrawingType = enum.DoodadDrawing
|
|
default:
|
|
return fmt.Errorf("file extension '%s' doesn't indicate its drawing type", ext)
|
|
}
|
|
|
|
d.Goto(scene)
|
|
return nil
|
|
}
|
|
|
|
// PlayLevel loads a map from JSON into the PlayScene.
|
|
func (d *Doodle) PlayLevel(filename string) error {
|
|
log.Info("Loading level from file: %s", filename)
|
|
scene := &PlayScene{
|
|
Filename: filename,
|
|
}
|
|
d.Goto(scene)
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|