doodle/pkg/doodle.go
Noah Petherbridge ddcad27485 WIP: Chunker size to uint8 and Rectangular Doodads
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.
2023-02-16 21:47:18 -08:00

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
}