409 lines
8.6 KiB
Go
409 lines
8.6 KiB
Go
package game
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
|
|
|
"github.com/atridad/LilGuy/internal/save"
|
|
"github.com/atridad/LilGuy/internal/screens"
|
|
"github.com/atridad/LilGuy/internal/ui/menu"
|
|
"github.com/atridad/LilGuy/internal/maps"
|
|
)
|
|
|
|
const (
|
|
ScreenWidth = 960
|
|
ScreenHeight = 540
|
|
TargetTPS = 60
|
|
WindowTitle = "Lil Guy"
|
|
)
|
|
|
|
// Game states
|
|
type gameState int
|
|
|
|
const (
|
|
stateSplash gameState = iota
|
|
stateTitle
|
|
statePlaying
|
|
statePaused
|
|
)
|
|
|
|
// FPS cap options
|
|
type FPSCap int
|
|
|
|
const (
|
|
FPSCap60 FPSCap = iota
|
|
FPSCap120
|
|
FPSCapUncapped
|
|
fpsCapCount
|
|
)
|
|
|
|
func (f FPSCap) TPS() int {
|
|
switch f {
|
|
case FPSCap60:
|
|
return 60
|
|
case FPSCap120:
|
|
return 120
|
|
case FPSCapUncapped:
|
|
return -1
|
|
default:
|
|
return 60
|
|
}
|
|
}
|
|
|
|
func (f FPSCap) String() string {
|
|
switch f {
|
|
case FPSCap60:
|
|
return "60 FPS"
|
|
case FPSCap120:
|
|
return "120 FPS"
|
|
case FPSCapUncapped:
|
|
return "Uncapped"
|
|
default:
|
|
return "60 FPS"
|
|
}
|
|
}
|
|
|
|
func (f *FPSCap) Cycle() {
|
|
*f = (*f + 1) % fpsCapCount
|
|
}
|
|
|
|
// Player input
|
|
type controls struct {
|
|
Left bool
|
|
Right bool
|
|
Jump bool
|
|
Sprint bool
|
|
Shoot bool
|
|
}
|
|
|
|
func readControls() controls {
|
|
return controls{
|
|
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
|
|
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
|
Jump: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeySpace),
|
|
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
|
|
Shoot: inpututil.IsKeyJustPressed(ebiten.KeyK),
|
|
}
|
|
}
|
|
|
|
type Game struct {
|
|
state *state
|
|
}
|
|
|
|
// Main game state
|
|
type state struct {
|
|
gameState gameState
|
|
lastTick time.Time
|
|
|
|
splashScreen *screens.SplashScreen
|
|
titleScreen *screens.TitleScreen
|
|
gameplayScreen *screens.GameplayScreen
|
|
pauseMenu *menu.PauseMenu
|
|
|
|
fpsEnabled bool
|
|
fpsCap FPSCap
|
|
portalVisibility bool
|
|
saveManager *save.Manager
|
|
|
|
lastAutoSave time.Time
|
|
autoSaveInterval time.Duration
|
|
}
|
|
|
|
func New() *Game {
|
|
return &Game{state: newState()}
|
|
}
|
|
|
|
func newState() *state {
|
|
now := time.Now()
|
|
s := &state{
|
|
gameState: stateSplash,
|
|
lastTick: now,
|
|
fpsEnabled: false,
|
|
fpsCap: FPSCap60,
|
|
portalVisibility: false,
|
|
lastAutoSave: now,
|
|
autoSaveInterval: 30 * time.Second,
|
|
}
|
|
|
|
saveManager, err := save.NewManager()
|
|
if err != nil {
|
|
saveManager = nil
|
|
}
|
|
s.saveManager = saveManager
|
|
|
|
if saveManager != nil {
|
|
if settings, err := saveManager.LoadSettings(); err == nil {
|
|
s.fpsEnabled = settings.FPSMonitor
|
|
s.portalVisibility = settings.PortalVisibility
|
|
switch settings.FPSCap {
|
|
case "60":
|
|
s.fpsCap = FPSCap60
|
|
case "120":
|
|
s.fpsCap = FPSCap120
|
|
case "uncapped":
|
|
s.fpsCap = FPSCapUncapped
|
|
default:
|
|
s.fpsCap = FPSCap60
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize Map Manager
|
|
mapManager := maps.NewManager()
|
|
plains, desert := maps.CreateDefaultMaps(float64(ScreenWidth), float64(ScreenHeight))
|
|
|
|
// Bake maps for performance
|
|
plains.Bake()
|
|
desert.Bake()
|
|
|
|
mapManager.RegisterMap(plains)
|
|
mapManager.RegisterMap(desert)
|
|
mapManager.SetCurrentMap("plains")
|
|
|
|
// Initialize screens
|
|
s.splashScreen = screens.NewSplashScreen()
|
|
s.titleScreen = screens.NewTitleScreen()
|
|
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, mapManager, &s.fpsEnabled, &s.portalVisibility)
|
|
s.pauseMenu = menu.NewPauseMenu()
|
|
|
|
// Wire up settings references
|
|
s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
|
|
s.titleScreen.SetFPSCap(&s.fpsCap)
|
|
s.titleScreen.SetPortalVisibility(&s.portalVisibility)
|
|
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
|
|
s.pauseMenu.SetFPSCap(&s.fpsCap)
|
|
s.pauseMenu.SetPortalVisibility(&s.portalVisibility)
|
|
|
|
if saveManager != nil {
|
|
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
|
|
}
|
|
|
|
ebiten.SetTPS(s.fpsCap.TPS())
|
|
|
|
// Create initial save file
|
|
if saveManager != nil {
|
|
settings := &save.Settings{
|
|
FPSMonitor: s.fpsEnabled,
|
|
FPSCap: s.fpCapToStringHelper(s.fpsCap),
|
|
PortalVisibility: s.portalVisibility,
|
|
}
|
|
saveManager.SaveSettings(settings)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *state) fpCapToStringHelper(cap FPSCap) string {
|
|
switch cap {
|
|
case FPSCap60:
|
|
return "60"
|
|
case FPSCap120:
|
|
return "120"
|
|
case FPSCapUncapped:
|
|
return "uncapped"
|
|
default:
|
|
return "60"
|
|
}
|
|
}
|
|
|
|
func (g *Game) Update() error {
|
|
prevFPSEnabled := g.state.fpsEnabled
|
|
prevFPSCap := g.state.fpsCap
|
|
prevPortalVisibility := g.state.portalVisibility
|
|
|
|
currentTPS := g.state.fpsCap.TPS()
|
|
if currentTPS < 0 {
|
|
ebiten.SetTPS(ebiten.SyncWithFPS)
|
|
} else {
|
|
ebiten.SetTPS(currentTPS)
|
|
}
|
|
|
|
// Update current screen
|
|
var err error
|
|
switch g.state.gameState {
|
|
case stateSplash:
|
|
err = g.updateSplash()
|
|
case stateTitle:
|
|
err = g.updateTitle()
|
|
case statePlaying:
|
|
err = g.updatePlaying()
|
|
case statePaused:
|
|
err = g.updatePaused()
|
|
}
|
|
|
|
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap || prevPortalVisibility != g.state.portalVisibility {
|
|
g.saveSettings()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Screen update handlers
|
|
|
|
func (g *Game) updateSplash() error {
|
|
if g.state.splashScreen.Update() {
|
|
g.state.gameState = stateTitle
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) updateTitle() error {
|
|
if selectedOption := g.state.titleScreen.Update(); selectedOption != nil {
|
|
switch *selectedOption {
|
|
case screens.TitleOptionContinue:
|
|
if g.state.saveManager != nil {
|
|
if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil {
|
|
g.state.gameplayScreen.LoadState(gameState)
|
|
g.state.gameState = statePlaying
|
|
g.state.lastTick = time.Now()
|
|
}
|
|
}
|
|
case screens.TitleOptionNewGame:
|
|
g.state.gameState = statePlaying
|
|
g.state.gameplayScreen.Reset()
|
|
g.state.lastTick = time.Now()
|
|
if g.state.saveManager != nil {
|
|
g.state.saveManager.DeleteGameState()
|
|
}
|
|
case screens.TitleOptionSettings:
|
|
case screens.TitleOptionQuit:
|
|
return ebiten.Termination
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) updatePlaying() error {
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
|
g.state.gameState = statePaused
|
|
g.state.pauseMenu.Reset()
|
|
return nil
|
|
}
|
|
|
|
now := time.Now()
|
|
delta := now.Sub(g.state.lastTick)
|
|
g.state.lastTick = now
|
|
|
|
maxDelta := 100 * time.Millisecond
|
|
if delta > maxDelta {
|
|
delta = maxDelta
|
|
}
|
|
|
|
minDelta := time.Microsecond
|
|
if delta < minDelta {
|
|
delta = minDelta
|
|
}
|
|
|
|
input := readControls()
|
|
g.state.gameplayScreen.Update(screens.GameplayInput{
|
|
Left: input.Left,
|
|
Right: input.Right,
|
|
Jump: input.Jump,
|
|
Sprint: input.Sprint,
|
|
Shoot: input.Shoot,
|
|
}, delta)
|
|
|
|
if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval {
|
|
g.saveGame()
|
|
g.state.lastAutoSave = now
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) updatePaused() error {
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
|
g.state.gameState = statePlaying
|
|
g.state.lastTick = time.Now()
|
|
return nil
|
|
}
|
|
|
|
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
|
|
switch *selectedOption {
|
|
case menu.OptionResume:
|
|
g.state.gameState = statePlaying
|
|
g.state.lastTick = time.Now()
|
|
case menu.OptionSave:
|
|
g.saveGame()
|
|
g.state.gameplayScreen.ShowSaveNotification()
|
|
g.state.gameState = statePlaying
|
|
g.state.lastTick = time.Now()
|
|
case menu.OptionMainMenu:
|
|
g.saveGame()
|
|
g.state.gameState = stateTitle
|
|
if g.state.saveManager != nil {
|
|
g.state.titleScreen.SetHasSaveGame(g.state.saveManager.HasSavedGame())
|
|
}
|
|
g.state.titleScreen.Reset()
|
|
case menu.OptionQuit:
|
|
g.saveGame()
|
|
return ebiten.Termination
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Save/load operations
|
|
|
|
func (g *Game) saveGame() {
|
|
if g.state.saveManager == nil {
|
|
return
|
|
}
|
|
|
|
if g.state.gameState == statePlaying || g.state.gameState == statePaused {
|
|
gameState := g.state.gameplayScreen.SaveState()
|
|
g.state.saveManager.SaveGameState(gameState)
|
|
}
|
|
|
|
g.saveSettings()
|
|
}
|
|
|
|
func (g *Game) saveSettings() {
|
|
if g.state.saveManager == nil {
|
|
return
|
|
}
|
|
|
|
settings := &save.Settings{
|
|
FPSMonitor: g.state.fpsEnabled,
|
|
FPSCap: g.fpCapToString(g.state.fpsCap),
|
|
PortalVisibility: g.state.portalVisibility,
|
|
}
|
|
g.state.saveManager.SaveSettings(settings)
|
|
}
|
|
|
|
func (g *Game) fpCapToString(cap FPSCap) string {
|
|
switch cap {
|
|
case FPSCap60:
|
|
return "60"
|
|
case FPSCap120:
|
|
return "120"
|
|
case FPSCapUncapped:
|
|
return "uncapped"
|
|
default:
|
|
return "60"
|
|
}
|
|
}
|
|
|
|
// Rendering
|
|
|
|
func (g *Game) Draw(screen *ebiten.Image) {
|
|
switch g.state.gameState {
|
|
case stateSplash:
|
|
g.state.splashScreen.Draw(screen, ScreenWidth, ScreenHeight)
|
|
case stateTitle:
|
|
g.state.titleScreen.Draw(screen, ScreenWidth, ScreenHeight)
|
|
case statePlaying:
|
|
g.state.gameplayScreen.Draw(screen)
|
|
case statePaused:
|
|
g.state.gameplayScreen.Draw(screen)
|
|
g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
|
|
}
|
|
}
|
|
|
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|
return ScreenWidth, ScreenHeight
|
|
}
|