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 }