Better commenting throughout
This commit is contained in:
@@ -11,8 +11,6 @@ import (
|
|||||||
"github.com/atridad/LilGuy/internal/ui/menu"
|
"github.com/atridad/LilGuy/internal/ui/menu"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Window and display configuration.
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ScreenWidth = 960
|
ScreenWidth = 960
|
||||||
ScreenHeight = 540
|
ScreenHeight = 540
|
||||||
@@ -20,29 +18,17 @@ const (
|
|||||||
WindowTitle = "Lil Guy"
|
WindowTitle = "Lil Guy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Game states define the different screens and modes the game can be in.
|
// Game states
|
||||||
// To add a new state:
|
|
||||||
// 1. Add a new constant to the gameState enum below
|
|
||||||
// 2. Create a new screen type in internal/screens/ (see splash.go, title.go, gameplay.go as examples)
|
|
||||||
// 3. Add the screen instance to the 'state' struct
|
|
||||||
// 4. Handle state transitions in Update() method
|
|
||||||
// 5. Handle rendering in Draw() method
|
|
||||||
//
|
|
||||||
// State Flow:
|
|
||||||
// stateSplash -> stateTitle -> statePlaying <-> statePaused
|
|
||||||
// ^____________|
|
|
||||||
|
|
||||||
type gameState int
|
type gameState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
stateSplash gameState = iota // Initial splash screen with game logo
|
stateSplash gameState = iota
|
||||||
stateTitle // Main menu (Play/Quit options)
|
stateTitle
|
||||||
statePlaying // Active gameplay
|
statePlaying
|
||||||
statePaused // Game paused (overlay menu)
|
statePaused
|
||||||
)
|
)
|
||||||
|
|
||||||
// FPS cap options for performance tuning.
|
// FPS cap options
|
||||||
|
|
||||||
type FPSCap int
|
type FPSCap int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -82,8 +68,7 @@ func (f *FPSCap) Cycle() {
|
|||||||
*f = (*f + 1) % fpsCapCount
|
*f = (*f + 1) % fpsCapCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input state for player controls.
|
// Player input
|
||||||
|
|
||||||
type controls struct {
|
type controls struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
@@ -106,34 +91,28 @@ type Game struct {
|
|||||||
state *state
|
state *state
|
||||||
}
|
}
|
||||||
|
|
||||||
// state holds all game state including screens, settings, and current mode.
|
// Main game state
|
||||||
type state struct {
|
type state struct {
|
||||||
// Current state
|
|
||||||
gameState gameState
|
gameState gameState
|
||||||
lastTick time.Time
|
lastTick time.Time
|
||||||
|
|
||||||
// Screens - each screen manages its own UI and logic
|
|
||||||
splashScreen *screens.SplashScreen
|
splashScreen *screens.SplashScreen
|
||||||
titleScreen *screens.TitleScreen
|
titleScreen *screens.TitleScreen
|
||||||
gameplayScreen *screens.GameplayScreen
|
gameplayScreen *screens.GameplayScreen
|
||||||
pauseMenu *menu.PauseMenu
|
pauseMenu *menu.PauseMenu
|
||||||
|
|
||||||
// Settings
|
|
||||||
fpsEnabled bool
|
fpsEnabled bool
|
||||||
fpsCap FPSCap
|
fpsCap FPSCap
|
||||||
saveManager *save.Manager
|
saveManager *save.Manager
|
||||||
|
|
||||||
// Auto-save
|
|
||||||
lastAutoSave time.Time
|
lastAutoSave time.Time
|
||||||
autoSaveInterval time.Duration
|
autoSaveInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new game instance.
|
|
||||||
func New() *Game {
|
func New() *Game {
|
||||||
return &Game{state: newState()}
|
return &Game{state: newState()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newState initializes a fresh game state.
|
|
||||||
func newState() *state {
|
func newState() *state {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
s := &state{
|
s := &state{
|
||||||
@@ -142,19 +121,15 @@ func newState() *state {
|
|||||||
fpsEnabled: false,
|
fpsEnabled: false,
|
||||||
fpsCap: FPSCap60,
|
fpsCap: FPSCap60,
|
||||||
lastAutoSave: now,
|
lastAutoSave: now,
|
||||||
autoSaveInterval: 30 * time.Second, // Auto-save every 30 seconds
|
autoSaveInterval: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize save manager
|
|
||||||
saveManager, err := save.NewManager()
|
saveManager, err := save.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If save manager fails, continue without it (settings won't persist)
|
|
||||||
// TODO: Show error to user
|
|
||||||
saveManager = nil
|
saveManager = nil
|
||||||
}
|
}
|
||||||
s.saveManager = saveManager
|
s.saveManager = saveManager
|
||||||
|
|
||||||
// Load settings if available
|
|
||||||
if saveManager != nil {
|
if saveManager != nil {
|
||||||
if settings, err := saveManager.LoadSettings(); err == nil {
|
if settings, err := saveManager.LoadSettings(); err == nil {
|
||||||
s.fpsEnabled = settings.FPSMonitor
|
s.fpsEnabled = settings.FPSMonitor
|
||||||
@@ -171,27 +146,25 @@ func newState() *state {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize all screens
|
// Initialize screens
|
||||||
s.splashScreen = screens.NewSplashScreen()
|
s.splashScreen = screens.NewSplashScreen()
|
||||||
s.titleScreen = screens.NewTitleScreen()
|
s.titleScreen = screens.NewTitleScreen()
|
||||||
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled)
|
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled)
|
||||||
s.pauseMenu = menu.NewPauseMenu()
|
s.pauseMenu = menu.NewPauseMenu()
|
||||||
|
|
||||||
// Configure settings references for title screen and pause menu
|
// Wire up settings references
|
||||||
s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
|
s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
|
||||||
s.titleScreen.SetFPSCap(&s.fpsCap)
|
s.titleScreen.SetFPSCap(&s.fpsCap)
|
||||||
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
|
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
|
||||||
s.pauseMenu.SetFPSCap(&s.fpsCap)
|
s.pauseMenu.SetFPSCap(&s.fpsCap)
|
||||||
|
|
||||||
// Check if saved game exists
|
|
||||||
if saveManager != nil {
|
if saveManager != nil {
|
||||||
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
|
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set initial TPS
|
|
||||||
ebiten.SetTPS(s.fpsCap.TPS())
|
ebiten.SetTPS(s.fpsCap.TPS())
|
||||||
|
|
||||||
// Save initial settings to create data.toml on first launch
|
// Create initial save file
|
||||||
if saveManager != nil {
|
if saveManager != nil {
|
||||||
settings := &save.Settings{
|
settings := &save.Settings{
|
||||||
FPSMonitor: s.fpsEnabled,
|
FPSMonitor: s.fpsEnabled,
|
||||||
@@ -203,7 +176,6 @@ func newState() *state {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for converting FPSCap to string (used in initialization)
|
|
||||||
func (s *state) fpCapToStringHelper(cap FPSCap) string {
|
func (s *state) fpCapToStringHelper(cap FPSCap) string {
|
||||||
switch cap {
|
switch cap {
|
||||||
case FPSCap60:
|
case FPSCap60:
|
||||||
@@ -217,14 +189,10 @@ func (s *state) fpCapToStringHelper(cap FPSCap) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is called every frame and handles state transitions and input.
|
|
||||||
|
|
||||||
func (g *Game) Update() error {
|
func (g *Game) Update() error {
|
||||||
// Track previous FPS settings to detect changes
|
|
||||||
prevFPSEnabled := g.state.fpsEnabled
|
prevFPSEnabled := g.state.fpsEnabled
|
||||||
prevFPSCap := g.state.fpsCap
|
prevFPSCap := g.state.fpsCap
|
||||||
|
|
||||||
// Update TPS if FPS cap changed
|
|
||||||
currentTPS := g.state.fpsCap.TPS()
|
currentTPS := g.state.fpsCap.TPS()
|
||||||
if currentTPS < 0 {
|
if currentTPS < 0 {
|
||||||
ebiten.SetTPS(ebiten.SyncWithFPS)
|
ebiten.SetTPS(ebiten.SyncWithFPS)
|
||||||
@@ -232,7 +200,7 @@ func (g *Game) Update() error {
|
|||||||
ebiten.SetTPS(currentTPS)
|
ebiten.SetTPS(currentTPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle state-specific updates
|
// Update current screen
|
||||||
var err error
|
var err error
|
||||||
switch g.state.gameState {
|
switch g.state.gameState {
|
||||||
case stateSplash:
|
case stateSplash:
|
||||||
@@ -245,7 +213,6 @@ func (g *Game) Update() error {
|
|||||||
err = g.updatePaused()
|
err = g.updatePaused()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-save settings if they changed
|
|
||||||
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap {
|
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap {
|
||||||
g.saveSettings()
|
g.saveSettings()
|
||||||
}
|
}
|
||||||
@@ -253,7 +220,8 @@ func (g *Game) Update() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateSplash handles the splash screen state.
|
// Screen update handlers
|
||||||
|
|
||||||
func (g *Game) updateSplash() error {
|
func (g *Game) updateSplash() error {
|
||||||
if g.state.splashScreen.Update() {
|
if g.state.splashScreen.Update() {
|
||||||
g.state.gameState = stateTitle
|
g.state.gameState = stateTitle
|
||||||
@@ -261,12 +229,10 @@ func (g *Game) updateSplash() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateTitle handles the title screen state.
|
|
||||||
func (g *Game) updateTitle() error {
|
func (g *Game) updateTitle() error {
|
||||||
if selectedOption := g.state.titleScreen.Update(); selectedOption != nil {
|
if selectedOption := g.state.titleScreen.Update(); selectedOption != nil {
|
||||||
switch *selectedOption {
|
switch *selectedOption {
|
||||||
case screens.TitleOptionContinue:
|
case screens.TitleOptionContinue:
|
||||||
// Load saved game
|
|
||||||
if g.state.saveManager != nil {
|
if g.state.saveManager != nil {
|
||||||
if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil {
|
if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil {
|
||||||
g.state.gameplayScreen.LoadState(gameState)
|
g.state.gameplayScreen.LoadState(gameState)
|
||||||
@@ -278,13 +244,10 @@ func (g *Game) updateTitle() error {
|
|||||||
g.state.gameState = statePlaying
|
g.state.gameState = statePlaying
|
||||||
g.state.gameplayScreen.Reset()
|
g.state.gameplayScreen.Reset()
|
||||||
g.state.lastTick = time.Now()
|
g.state.lastTick = time.Now()
|
||||||
// Delete old save if it exists
|
|
||||||
if g.state.saveManager != nil {
|
if g.state.saveManager != nil {
|
||||||
g.state.saveManager.DeleteGameState()
|
g.state.saveManager.DeleteGameState()
|
||||||
}
|
}
|
||||||
case screens.TitleOptionSettings:
|
case screens.TitleOptionSettings:
|
||||||
// Settings are handled within the title screen itself
|
|
||||||
// No state change needed
|
|
||||||
case screens.TitleOptionQuit:
|
case screens.TitleOptionQuit:
|
||||||
return ebiten.Termination
|
return ebiten.Termination
|
||||||
}
|
}
|
||||||
@@ -292,21 +255,17 @@ func (g *Game) updateTitle() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updatePlaying handles the active gameplay state.
|
|
||||||
func (g *Game) updatePlaying() error {
|
func (g *Game) updatePlaying() error {
|
||||||
// Check for pause
|
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
g.state.gameState = statePaused
|
g.state.gameState = statePaused
|
||||||
g.state.pauseMenu.Reset()
|
g.state.pauseMenu.Reset()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate delta time
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
delta := now.Sub(g.state.lastTick)
|
delta := now.Sub(g.state.lastTick)
|
||||||
g.state.lastTick = now
|
g.state.lastTick = now
|
||||||
|
|
||||||
// Update gameplay
|
|
||||||
input := readControls()
|
input := readControls()
|
||||||
g.state.gameplayScreen.Update(screens.GameplayInput{
|
g.state.gameplayScreen.Update(screens.GameplayInput{
|
||||||
Left: input.Left,
|
Left: input.Left,
|
||||||
@@ -316,7 +275,6 @@ func (g *Game) updatePlaying() error {
|
|||||||
Shoot: input.Shoot,
|
Shoot: input.Shoot,
|
||||||
}, delta)
|
}, delta)
|
||||||
|
|
||||||
// Periodic auto-save
|
|
||||||
if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval {
|
if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval {
|
||||||
g.saveGame()
|
g.saveGame()
|
||||||
g.state.gameplayScreen.ShowSaveNotification()
|
g.state.gameplayScreen.ShowSaveNotification()
|
||||||
@@ -326,29 +284,24 @@ func (g *Game) updatePlaying() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updatePaused handles the pause menu state.
|
|
||||||
func (g *Game) updatePaused() error {
|
func (g *Game) updatePaused() error {
|
||||||
// Allow ESC to resume
|
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
g.state.gameState = statePlaying
|
g.state.gameState = statePlaying
|
||||||
g.state.lastTick = time.Now()
|
g.state.lastTick = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle pause menu selection
|
|
||||||
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
|
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
|
||||||
switch *selectedOption {
|
switch *selectedOption {
|
||||||
case menu.OptionResume:
|
case menu.OptionResume:
|
||||||
g.state.gameState = statePlaying
|
g.state.gameState = statePlaying
|
||||||
g.state.lastTick = time.Now()
|
g.state.lastTick = time.Now()
|
||||||
case menu.OptionSave:
|
case menu.OptionSave:
|
||||||
// Save game immediately
|
|
||||||
g.saveGame()
|
g.saveGame()
|
||||||
g.state.gameplayScreen.ShowSaveNotification()
|
g.state.gameplayScreen.ShowSaveNotification()
|
||||||
g.state.gameState = statePlaying
|
g.state.gameState = statePlaying
|
||||||
g.state.lastTick = time.Now()
|
g.state.lastTick = time.Now()
|
||||||
case menu.OptionMainMenu:
|
case menu.OptionMainMenu:
|
||||||
// Save game before returning to main menu
|
|
||||||
g.saveGame()
|
g.saveGame()
|
||||||
g.state.gameState = stateTitle
|
g.state.gameState = stateTitle
|
||||||
if g.state.saveManager != nil {
|
if g.state.saveManager != nil {
|
||||||
@@ -356,7 +309,6 @@ func (g *Game) updatePaused() error {
|
|||||||
}
|
}
|
||||||
g.state.titleScreen.Reset()
|
g.state.titleScreen.Reset()
|
||||||
case menu.OptionQuit:
|
case menu.OptionQuit:
|
||||||
// Save game before quitting
|
|
||||||
g.saveGame()
|
g.saveGame()
|
||||||
return ebiten.Termination
|
return ebiten.Termination
|
||||||
}
|
}
|
||||||
@@ -365,23 +317,21 @@ func (g *Game) updatePaused() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveGame saves the current game state and settings.
|
// Save/load operations
|
||||||
|
|
||||||
func (g *Game) saveGame() {
|
func (g *Game) saveGame() {
|
||||||
if g.state.saveManager == nil {
|
if g.state.saveManager == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save game state if in playing mode
|
|
||||||
if g.state.gameState == statePlaying || g.state.gameState == statePaused {
|
if g.state.gameState == statePlaying || g.state.gameState == statePaused {
|
||||||
gameState := g.state.gameplayScreen.SaveState()
|
gameState := g.state.gameplayScreen.SaveState()
|
||||||
g.state.saveManager.SaveGameState(gameState)
|
g.state.saveManager.SaveGameState(gameState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings
|
|
||||||
g.saveSettings()
|
g.saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveSettings saves only the settings.
|
|
||||||
func (g *Game) saveSettings() {
|
func (g *Game) saveSettings() {
|
||||||
if g.state.saveManager == nil {
|
if g.state.saveManager == nil {
|
||||||
return
|
return
|
||||||
@@ -394,7 +344,6 @@ func (g *Game) saveSettings() {
|
|||||||
g.state.saveManager.SaveSettings(settings)
|
g.state.saveManager.SaveSettings(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fpCapToString converts FPSCap to string for saving.
|
|
||||||
func (g *Game) fpCapToString(cap FPSCap) string {
|
func (g *Game) fpCapToString(cap FPSCap) string {
|
||||||
switch cap {
|
switch cap {
|
||||||
case FPSCap60:
|
case FPSCap60:
|
||||||
@@ -408,7 +357,7 @@ func (g *Game) fpCapToString(cap FPSCap) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw renders the current game state to the screen.
|
// Rendering
|
||||||
|
|
||||||
func (g *Game) Draw(screen *ebiten.Image) {
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
switch g.state.gameState {
|
switch g.state.gameState {
|
||||||
@@ -419,13 +368,11 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
case statePlaying:
|
case statePlaying:
|
||||||
g.state.gameplayScreen.Draw(screen)
|
g.state.gameplayScreen.Draw(screen)
|
||||||
case statePaused:
|
case statePaused:
|
||||||
// Draw gameplay in background, then overlay pause menu
|
|
||||||
g.state.gameplayScreen.Draw(screen)
|
g.state.gameplayScreen.Draw(screen)
|
||||||
g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
|
g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout returns the game's logical screen size.
|
|
||||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
return ScreenWidth, ScreenHeight
|
return ScreenWidth, ScreenHeight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/atridad/LilGuy/internal/projectile"
|
"github.com/atridad/LilGuy/internal/projectile"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Hero defaults
|
||||||
const (
|
const (
|
||||||
defaultRadius = 24.0
|
defaultRadius = 24.0
|
||||||
defaultSpeed = 200.0
|
defaultSpeed = 200.0
|
||||||
@@ -36,6 +37,8 @@ const (
|
|||||||
airFriction = 0.95
|
airFriction = 0.95
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Input and bounds
|
||||||
|
|
||||||
type Input struct {
|
type Input struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
@@ -64,6 +67,8 @@ const (
|
|||||||
DirRight
|
DirRight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Hero state
|
||||||
|
|
||||||
type Hero struct {
|
type Hero struct {
|
||||||
X float64
|
X float64
|
||||||
Y float64
|
Y float64
|
||||||
@@ -155,6 +160,8 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
|||||||
h.updateAnimation(dt)
|
h.updateAnimation(dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Movement and physics
|
||||||
|
|
||||||
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||||
h.VelocityY += gravity * dt
|
h.VelocityY += gravity * dt
|
||||||
if h.VelocityY > maxFallSpeed {
|
if h.VelocityY > maxFallSpeed {
|
||||||
@@ -236,6 +243,8 @@ func (h *Hero) updateStamina(input Input, dt float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
|
||||||
func (h *Hero) updateAnimation(dt float64) {
|
func (h *Hero) updateAnimation(dt float64) {
|
||||||
isMoving := h.isMoving
|
isMoving := h.isMoving
|
||||||
key := animationKey{direction: h.direction, state: animIdle}
|
key := animationKey{direction: h.direction, state: animIdle}
|
||||||
@@ -275,6 +284,8 @@ func (h *Hero) getVisualState() VisualState {
|
|||||||
return StateIdle
|
return StateIdle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
|
||||||
func (h *Hero) Draw(screen *ebiten.Image) {
|
func (h *Hero) Draw(screen *ebiten.Image) {
|
||||||
sprite := h.getCurrentSprite()
|
sprite := h.getCurrentSprite()
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Asset paths
|
||||||
const (
|
const (
|
||||||
heroDir = "assets/hero"
|
heroDir = "assets/hero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Animation state
|
||||||
type animState int
|
type animState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,6 +29,7 @@ type animationKey struct {
|
|||||||
state animState
|
state animState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sprite cache
|
||||||
var (
|
var (
|
||||||
knightAnimations map[animationKey][]*ebiten.Image
|
knightAnimations map[animationKey][]*ebiten.Image
|
||||||
)
|
)
|
||||||
@@ -47,6 +50,8 @@ func getKnightSprite(direction Direction, moving bool, frameIndex int) *ebiten.I
|
|||||||
return frameFromSet(direction, state, frameIndex)
|
return frameFromSet(direction, state, frameIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Asset loading
|
||||||
|
|
||||||
func loadKnightAnimations() error {
|
func loadKnightAnimations() error {
|
||||||
frames, err := loadAnimationFrames(heroDir)
|
frames, err := loadAnimationFrames(heroDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,6 +118,8 @@ func loadImage(path string) (*ebiten.Image, error) {
|
|||||||
return ebiten.NewImageFromImage(img), nil
|
return ebiten.NewImageFromImage(img), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image manipulation
|
||||||
|
|
||||||
func flipImageHorizontally(img *ebiten.Image) *ebiten.Image {
|
func flipImageHorizontally(img *ebiten.Image) *ebiten.Image {
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
w, h := bounds.Dx(), bounds.Dy()
|
w, h := bounds.Dx(), bounds.Dy()
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Projectile configuration
|
||||||
|
|
||||||
type ProjectileConfig struct {
|
type ProjectileConfig struct {
|
||||||
Speed float64
|
Speed float64
|
||||||
Radius float64
|
Radius float64
|
||||||
@@ -58,6 +60,8 @@ func (p *Projectile) Draw(screen *ebiten.Image) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Projectile manager
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
Projectiles []*Projectile
|
Projectiles []*Projectile
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,47 +9,43 @@ import (
|
|||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// File path for save data.
|
|
||||||
const (
|
const (
|
||||||
dataFile = "data.toml"
|
dataFile = "data.toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stores all persistent game data in a single file.
|
// Save file structure
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Settings Settings `toml:"settings"`
|
Settings Settings `toml:"settings"`
|
||||||
GameState GameState `toml:"game_state"`
|
GameState GameState `toml:"game_state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stores user preferences.
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
FPSMonitor bool `toml:"fps_monitor"`
|
FPSMonitor bool `toml:"fps_monitor"`
|
||||||
FPSCap string `toml:"fps_cap"` // "60", "120", or "uncapped"
|
FPSCap string `toml:"fps_cap"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stores the current game progress.
|
|
||||||
type GameState struct {
|
type GameState struct {
|
||||||
HasSave bool `toml:"has_save"`
|
HasSave bool `toml:"has_save"`
|
||||||
SavedAt time.Time `toml:"saved_at"`
|
SavedAt time.Time `toml:"saved_at"`
|
||||||
HeroX float64 `toml:"hero_x"`
|
HeroX float64 `toml:"hero_x"`
|
||||||
HeroY float64 `toml:"hero_y"`
|
HeroY float64 `toml:"hero_y"`
|
||||||
HeroStamina float64 `toml:"hero_stamina"`
|
HeroStamina float64 `toml:"hero_stamina"`
|
||||||
PlayTimeMS int64 `toml:"play_time_ms"` // Total play time in milliseconds
|
PlayTimeMS int64 `toml:"play_time_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles saving and loading of settings and game state.
|
// Save manager
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
dataPath string
|
dataPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new save manager.
|
|
||||||
func NewManager() (*Manager, error) {
|
func NewManager() (*Manager, error) {
|
||||||
// Get executable path
|
|
||||||
exePath, err := os.Executable()
|
exePath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get directory containing the executable
|
|
||||||
exeDir := filepath.Dir(exePath)
|
exeDir := filepath.Dir(exePath)
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
@@ -57,11 +53,10 @@ func NewManager() (*Manager, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads all data from disk.
|
// Data operations
|
||||||
|
|
||||||
func (m *Manager) LoadData() (*Data, error) {
|
func (m *Manager) LoadData() (*Data, error) {
|
||||||
// Check if file exists
|
|
||||||
if _, err := os.Stat(m.dataPath); os.IsNotExist(err) {
|
if _, err := os.Stat(m.dataPath); os.IsNotExist(err) {
|
||||||
// Return default data
|
|
||||||
return &Data{
|
return &Data{
|
||||||
Settings: Settings{
|
Settings: Settings{
|
||||||
FPSMonitor: false,
|
FPSMonitor: false,
|
||||||
@@ -81,7 +76,6 @@ func (m *Manager) LoadData() (*Data, error) {
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes all data to disk.
|
|
||||||
func (m *Manager) SaveData(data *Data) error {
|
func (m *Manager) SaveData(data *Data) error {
|
||||||
file, err := os.Create(m.dataPath)
|
file, err := os.Create(m.dataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,7 +91,8 @@ func (m *Manager) SaveData(data *Data) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads user settings from disk.
|
// Settings operations
|
||||||
|
|
||||||
func (m *Manager) LoadSettings() (*Settings, error) {
|
func (m *Manager) LoadSettings() (*Settings, error) {
|
||||||
data, err := m.LoadData()
|
data, err := m.LoadData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -106,11 +101,9 @@ func (m *Manager) LoadSettings() (*Settings, error) {
|
|||||||
return &data.Settings, nil
|
return &data.Settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes user settings to disk.
|
|
||||||
func (m *Manager) SaveSettings(settings *Settings) error {
|
func (m *Manager) SaveSettings(settings *Settings) error {
|
||||||
data, err := m.LoadData()
|
data, err := m.LoadData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If load fails, create new data with these settings
|
|
||||||
data = &Data{
|
data = &Data{
|
||||||
Settings: *settings,
|
Settings: *settings,
|
||||||
GameState: GameState{
|
GameState: GameState{
|
||||||
@@ -124,7 +117,8 @@ func (m *Manager) SaveSettings(settings *Settings) error {
|
|||||||
return m.SaveData(data)
|
return m.SaveData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads game state from disk.
|
// Game state operations
|
||||||
|
|
||||||
func (m *Manager) LoadGameState() (*GameState, error) {
|
func (m *Manager) LoadGameState() (*GameState, error) {
|
||||||
data, err := m.LoadData()
|
data, err := m.LoadData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -132,17 +126,15 @@ func (m *Manager) LoadGameState() (*GameState, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !data.GameState.HasSave {
|
if !data.GameState.HasSave {
|
||||||
return nil, nil // No save exists
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &data.GameState, nil
|
return &data.GameState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes game state to disk.
|
|
||||||
func (m *Manager) SaveGameState(state *GameState) error {
|
func (m *Manager) SaveGameState(state *GameState) error {
|
||||||
data, err := m.LoadData()
|
data, err := m.LoadData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If load fails, create new data with this game state
|
|
||||||
data = &Data{
|
data = &Data{
|
||||||
Settings: Settings{
|
Settings: Settings{
|
||||||
FPSMonitor: false,
|
FPSMonitor: false,
|
||||||
@@ -160,7 +152,6 @@ func (m *Manager) SaveGameState(state *GameState) error {
|
|||||||
return m.SaveData(data)
|
return m.SaveData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if a saved game exists.
|
|
||||||
func (m *Manager) HasSavedGame() bool {
|
func (m *Manager) HasSavedGame() bool {
|
||||||
data, err := m.LoadData()
|
data, err := m.LoadData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,7 +160,6 @@ func (m *Manager) HasSavedGame() bool {
|
|||||||
return data.GameState.HasSave
|
return data.GameState.HasSave
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes the saved game.
|
|
||||||
func (m *Manager) DeleteGameState() error {
|
func (m *Manager) DeleteGameState() error {
|
||||||
data, err := m.LoadData()
|
data, err := m.LoadData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,7 +173,8 @@ func (m *Manager) DeleteGameState() error {
|
|||||||
return m.SaveData(data)
|
return m.SaveData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the path to the save file.
|
// Utilities
|
||||||
|
|
||||||
func (m *Manager) GetSaveFilePath() string {
|
func (m *Manager) GetSaveFilePath() string {
|
||||||
return m.dataPath
|
return m.dataPath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/atridad/LilGuy/internal/world"
|
"github.com/atridad/LilGuy/internal/world"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Screen and hero defaults
|
||||||
var (
|
var (
|
||||||
backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
|
backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
|
||||||
saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, A: 255}
|
saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, A: 255}
|
||||||
@@ -38,13 +39,12 @@ var (
|
|||||||
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
// HUD settings.
|
// HUD positioning and colors
|
||||||
const (
|
const (
|
||||||
hudX = 960 - 220 // ScreenWidth - 220
|
hudX = 960 - 220
|
||||||
hudY = 20
|
hudY = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
// HUD colors.
|
|
||||||
var (
|
var (
|
||||||
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
|
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
|
||||||
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
|
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
|
||||||
@@ -53,6 +53,7 @@ var (
|
|||||||
fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255}
|
fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FPS monitoring thresholds
|
||||||
const (
|
const (
|
||||||
staminaLowThreshold = 0.2
|
staminaLowThreshold = 0.2
|
||||||
fpsWarnThreshold = 0.85
|
fpsWarnThreshold = 0.85
|
||||||
@@ -61,7 +62,6 @@ const (
|
|||||||
targetTPS = 60
|
targetTPS = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
// Player input for the gameplay screen.
|
|
||||||
type GameplayInput struct {
|
type GameplayInput struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
@@ -70,8 +70,6 @@ type GameplayInput struct {
|
|||||||
Shoot bool
|
Shoot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manages the main gameplay state including the hero, HUD, and game world.
|
|
||||||
// This is where the actual game logic and rendering happens during active play.
|
|
||||||
type GameplayScreen struct {
|
type GameplayScreen struct {
|
||||||
hero *hero.Hero
|
hero *hero.Hero
|
||||||
hud hud.Overlay
|
hud hud.Overlay
|
||||||
@@ -90,7 +88,6 @@ type GameplayScreen struct {
|
|||||||
showSaveNotification bool
|
showSaveNotification bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new gameplay screen instance.
|
|
||||||
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen {
|
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen {
|
||||||
w := world.NewWorld()
|
w := world.NewWorld()
|
||||||
|
|
||||||
@@ -133,7 +130,6 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *Gamepla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes gameplay logic with the given input and delta time.
|
|
||||||
func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
||||||
dt := delta.Seconds()
|
dt := delta.Seconds()
|
||||||
|
|
||||||
@@ -166,7 +162,7 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculates the current FPS if FPS monitoring is enabled.
|
// FPS tracking
|
||||||
func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
||||||
if g.fpsEnabled == nil || !*g.fpsEnabled {
|
if g.fpsEnabled == nil || !*g.fpsEnabled {
|
||||||
return
|
return
|
||||||
@@ -182,7 +178,7 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the gameplay screen.
|
// Rendering
|
||||||
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||||
screen.Fill(backgroundColor)
|
screen.Fill(backgroundColor)
|
||||||
|
|
||||||
@@ -205,7 +201,6 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
|||||||
meters := []status.Meter{staminaMeter}
|
meters := []status.Meter{staminaMeter}
|
||||||
|
|
||||||
if g.fpsEnabled != nil && *g.fpsEnabled {
|
if g.fpsEnabled != nil && *g.fpsEnabled {
|
||||||
// Color based on target FPS (60).
|
|
||||||
ratio := g.fpsValue / float64(targetTPS)
|
ratio := g.fpsValue / float64(targetTPS)
|
||||||
fpsColor := fpsGoodColor
|
fpsColor := fpsGoodColor
|
||||||
switch {
|
switch {
|
||||||
@@ -217,7 +212,7 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
|||||||
|
|
||||||
fpsMeter := status.Meter{
|
fpsMeter := status.Meter{
|
||||||
Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue),
|
Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue),
|
||||||
Base: -1, // Negative base means text-only display.
|
Base: -1,
|
||||||
Level: 0,
|
Level: 0,
|
||||||
Color: fpsColor,
|
Color: fpsColor,
|
||||||
}
|
}
|
||||||
@@ -266,7 +261,7 @@ func (g *GameplayScreen) ShowSaveNotification() {
|
|||||||
g.saveNotificationTimer = saveNotificationDuration
|
g.saveNotificationTimer = saveNotificationDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets the gameplay screen to its initial state.
|
// State management
|
||||||
func (g *GameplayScreen) Reset() {
|
func (g *GameplayScreen) Reset() {
|
||||||
screenWidth := int(g.bounds.Width)
|
screenWidth := int(g.bounds.Width)
|
||||||
screenHeight := int(g.bounds.Height)
|
screenHeight := int(g.bounds.Height)
|
||||||
@@ -307,7 +302,6 @@ func (g *GameplayScreen) Reset() {
|
|||||||
g.fpsValue = 0
|
g.fpsValue = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveState converts the current gameplay state to a saveable format.
|
|
||||||
func (g *GameplayScreen) SaveState() *save.GameState {
|
func (g *GameplayScreen) SaveState() *save.GameState {
|
||||||
return &save.GameState{
|
return &save.GameState{
|
||||||
HeroX: g.hero.X,
|
HeroX: g.hero.X,
|
||||||
@@ -317,7 +311,6 @@ func (g *GameplayScreen) SaveState() *save.GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadState restores gameplay state from saved data.
|
|
||||||
func (g *GameplayScreen) LoadState(state *save.GameState) {
|
func (g *GameplayScreen) LoadState(state *save.GameState) {
|
||||||
screenWidth := int(g.bounds.Width)
|
screenWidth := int(g.bounds.Width)
|
||||||
screenHeight := int(g.bounds.Height)
|
screenHeight := int(g.bounds.Height)
|
||||||
|
|||||||
@@ -8,39 +8,32 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An interface for managing FPS cap settings.
|
// Settings interface
|
||||||
type FPSCapSetting interface {
|
type FPSCapSetting interface {
|
||||||
String() string
|
String() string
|
||||||
Cycle()
|
Cycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Displays and manages game settings like FPS monitor and FPS cap.
|
|
||||||
// This screen can be accessed from both the title screen and pause menu.
|
|
||||||
type SettingsScreen struct {
|
type SettingsScreen struct {
|
||||||
selectedIndex int
|
selectedIndex int
|
||||||
fpsMonitorValue *bool
|
fpsMonitorValue *bool
|
||||||
fpsCapValue FPSCapSetting
|
fpsCapValue FPSCapSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new settings screen instance.
|
|
||||||
func NewSettingsScreen() *SettingsScreen {
|
func NewSettingsScreen() *SettingsScreen {
|
||||||
return &SettingsScreen{
|
return &SettingsScreen{
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the FPS monitor toggle reference.
|
|
||||||
func (s *SettingsScreen) SetFPSMonitor(enabled *bool) {
|
func (s *SettingsScreen) SetFPSMonitor(enabled *bool) {
|
||||||
s.fpsMonitorValue = enabled
|
s.fpsMonitorValue = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the FPS cap setting reference.
|
|
||||||
func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) {
|
func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) {
|
||||||
s.fpsCapValue = cap
|
s.fpsCapValue = cap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes settings screen input.
|
|
||||||
// Returns true if the user wants to go back.
|
|
||||||
func (s *SettingsScreen) Update() bool {
|
func (s *SettingsScreen) Update() bool {
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
return true
|
return true
|
||||||
@@ -71,20 +64,19 @@ func (s *SettingsScreen) Update() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the settings screen.
|
// Rendering
|
||||||
|
|
||||||
func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int, title string) {
|
func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int, title string) {
|
||||||
// Draw background
|
|
||||||
screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
|
screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
|
||||||
|
|
||||||
// Draw title
|
|
||||||
titleX := (screenWidth / 2) - (len(title) * 7 / 2)
|
titleX := (screenWidth / 2) - (len(title) * 7 / 2)
|
||||||
titleY := screenHeight/3 - 50
|
titleY := screenHeight/3 - 50
|
||||||
s.drawText(screen, title, color.White, titleX, titleY)
|
s.drawText(screen, title, color.White, titleX, titleY)
|
||||||
|
|
||||||
// Draw settings options
|
|
||||||
startY := screenHeight/2 - 20
|
startY := screenHeight/2 - 20
|
||||||
leftMargin := screenWidth/2 - 120
|
leftMargin := screenWidth/2 - 120
|
||||||
|
|
||||||
|
// FPS monitor toggle
|
||||||
fpsMonitorText := "FPS Monitor: "
|
fpsMonitorText := "FPS Monitor: "
|
||||||
if s.fpsMonitorValue != nil && *s.fpsMonitorValue {
|
if s.fpsMonitorValue != nil && *s.fpsMonitorValue {
|
||||||
fpsMonitorText += "ON"
|
fpsMonitorText += "ON"
|
||||||
@@ -100,6 +92,7 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in
|
|||||||
s.drawText(screen, fpsMonitorText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, startY)
|
s.drawText(screen, fpsMonitorText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, startY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FPS cap setting
|
||||||
fpsCapText := "FPS Cap: "
|
fpsCapText := "FPS Cap: "
|
||||||
if s.fpsCapValue != nil {
|
if s.fpsCapValue != nil {
|
||||||
fpsCapText += s.fpsCapValue.String()
|
fpsCapText += s.fpsCapValue.String()
|
||||||
@@ -116,7 +109,7 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in
|
|||||||
s.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, capY)
|
s.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, capY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw hint text
|
// Instructions
|
||||||
hintText := "Enter/Space to toggle, ESC to go back"
|
hintText := "Enter/Space to toggle, ESC to go back"
|
||||||
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
||||||
hintY := screenHeight - 50
|
hintY := screenHeight - 50
|
||||||
@@ -130,7 +123,6 @@ func (s *SettingsScreen) drawText(screen *ebiten.Image, txt string, clr color.Co
|
|||||||
text.Draw(screen, txt, basicFace, op)
|
text.Draw(screen, txt, basicFace, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets the settings screen to its initial state.
|
|
||||||
func (s *SettingsScreen) Reset() {
|
func (s *SettingsScreen) Reset() {
|
||||||
s.selectedIndex = 0
|
s.selectedIndex = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ var (
|
|||||||
basicFaceAscent = basicFace.Metrics().HAscent
|
basicFaceAscent = basicFace.Metrics().HAscent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
|
||||||
const (
|
const (
|
||||||
splashDuration = 2 * time.Second
|
splashDuration = 2 * time.Second
|
||||||
fadeInDuration = 500 * time.Millisecond
|
fadeInDuration = 500 * time.Millisecond
|
||||||
fadeOutDuration = 500 * time.Millisecond
|
fadeOutDuration = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
// Displays the game title with fade in/out effects.
|
|
||||||
// This is typically the first screen shown when the game starts.
|
|
||||||
type SplashScreen struct {
|
type SplashScreen struct {
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
fadeInEnd time.Time
|
fadeInEnd time.Time
|
||||||
@@ -30,7 +30,6 @@ type SplashScreen struct {
|
|||||||
endTime time.Time
|
endTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new splash screen instance.
|
|
||||||
func NewSplashScreen() *SplashScreen {
|
func NewSplashScreen() *SplashScreen {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return &SplashScreen{
|
return &SplashScreen{
|
||||||
@@ -41,15 +40,11 @@ func NewSplashScreen() *SplashScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes splash screen logic.
|
|
||||||
// Returns true when the splash screen should end and transition to the next screen.
|
|
||||||
func (s *SplashScreen) Update() bool {
|
func (s *SplashScreen) Update() bool {
|
||||||
// Return true if splash is complete
|
|
||||||
if time.Now().After(s.endTime) {
|
if time.Now().After(s.endTime) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow skipping with any key
|
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeySpace) ||
|
if inpututil.IsKeyJustPressed(ebiten.KeySpace) ||
|
||||||
inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
|
inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
|
||||||
inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
@@ -59,14 +54,14 @@ func (s *SplashScreen) Update() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the splash screen.
|
// Rendering
|
||||||
|
|
||||||
func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||||
screen.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 255})
|
screen.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 255})
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
alpha := 1.0
|
alpha := 1.0
|
||||||
|
|
||||||
// Calculate fade in/out
|
|
||||||
if now.Before(s.fadeInEnd) {
|
if now.Before(s.fadeInEnd) {
|
||||||
elapsed := now.Sub(s.startTime)
|
elapsed := now.Sub(s.startTime)
|
||||||
alpha = float64(elapsed) / float64(fadeInDuration)
|
alpha = float64(elapsed) / float64(fadeInDuration)
|
||||||
@@ -81,10 +76,9 @@ func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
|||||||
alpha = 1
|
alpha = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw large game title
|
// Draw title
|
||||||
titleText := "LIL GUY"
|
titleText := "LIL GUY"
|
||||||
|
|
||||||
// Calculate size for large text (scale up the basic font)
|
|
||||||
scale := 4.0
|
scale := 4.0
|
||||||
charWidth := 7.0 * scale
|
charWidth := 7.0 * scale
|
||||||
textWidth := float64(len(titleText)) * charWidth
|
textWidth := float64(len(titleText)) * charWidth
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Represents the options available on the title screen.
|
// Menu options
|
||||||
type TitleMenuOption int
|
type TitleMenuOption int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,6 +20,7 @@ const (
|
|||||||
titleOptionCount
|
titleOptionCount
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Screen modes
|
||||||
type titleScreenMode int
|
type titleScreenMode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,8 +28,6 @@ const (
|
|||||||
titleModeSettings
|
titleModeSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
// Displays the main menu with Continue, New Game, Settings, and Quit options.
|
|
||||||
// This is shown after the splash screen and when returning from the pause menu.
|
|
||||||
type TitleScreen struct {
|
type TitleScreen struct {
|
||||||
selectedIndex int
|
selectedIndex int
|
||||||
currentMode titleScreenMode
|
currentMode titleScreenMode
|
||||||
@@ -36,7 +35,6 @@ type TitleScreen struct {
|
|||||||
hasSaveGame bool
|
hasSaveGame bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new title screen instance.
|
|
||||||
func NewTitleScreen() *TitleScreen {
|
func NewTitleScreen() *TitleScreen {
|
||||||
return &TitleScreen{
|
return &TitleScreen{
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
@@ -46,28 +44,22 @@ func NewTitleScreen() *TitleScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the FPS monitor toggle reference for settings.
|
|
||||||
func (t *TitleScreen) SetFPSMonitor(enabled *bool) {
|
func (t *TitleScreen) SetFPSMonitor(enabled *bool) {
|
||||||
t.settingsScreen.SetFPSMonitor(enabled)
|
t.settingsScreen.SetFPSMonitor(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the FPS cap setting reference for settings.
|
|
||||||
func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) {
|
func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) {
|
||||||
t.settingsScreen.SetFPSCap(cap)
|
t.settingsScreen.SetFPSCap(cap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets whether a saved game exists.
|
|
||||||
func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
|
func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
|
||||||
t.hasSaveGame = hasSave
|
t.hasSaveGame = hasSave
|
||||||
// If no save game, skip Continue option
|
|
||||||
if !hasSave && t.selectedIndex == 0 {
|
if !hasSave && t.selectedIndex == 0 {
|
||||||
t.selectedIndex = 1 // Move to New Game
|
t.selectedIndex = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes title screen input and returns the selected option if any.
|
|
||||||
func (t *TitleScreen) Update() *TitleMenuOption {
|
func (t *TitleScreen) Update() *TitleMenuOption {
|
||||||
// Handle settings screen
|
|
||||||
if t.currentMode == titleModeSettings {
|
if t.currentMode == titleModeSettings {
|
||||||
if t.settingsScreen.Update() {
|
if t.settingsScreen.Update() {
|
||||||
t.currentMode = titleModeMain
|
t.currentMode = titleModeMain
|
||||||
@@ -76,13 +68,11 @@ func (t *TitleScreen) Update() *TitleMenuOption {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle main menu
|
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
|
||||||
t.selectedIndex--
|
t.selectedIndex--
|
||||||
if t.selectedIndex < 0 {
|
if t.selectedIndex < 0 {
|
||||||
t.selectedIndex = int(titleOptionCount) - 1
|
t.selectedIndex = int(titleOptionCount) - 1
|
||||||
}
|
}
|
||||||
// Skip Continue if no save exists
|
|
||||||
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
|
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||||
t.selectedIndex--
|
t.selectedIndex--
|
||||||
if t.selectedIndex < 0 {
|
if t.selectedIndex < 0 {
|
||||||
@@ -96,7 +86,6 @@ func (t *TitleScreen) Update() *TitleMenuOption {
|
|||||||
if t.selectedIndex >= int(titleOptionCount) {
|
if t.selectedIndex >= int(titleOptionCount) {
|
||||||
t.selectedIndex = 0
|
t.selectedIndex = 0
|
||||||
}
|
}
|
||||||
// Skip Continue if no save exists
|
|
||||||
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
|
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||||
t.selectedIndex++
|
t.selectedIndex++
|
||||||
if t.selectedIndex >= int(titleOptionCount) {
|
if t.selectedIndex >= int(titleOptionCount) {
|
||||||
@@ -118,18 +107,15 @@ func (t *TitleScreen) Update() *TitleMenuOption {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the title screen.
|
// Rendering
|
||||||
func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||||
// If in settings mode, draw settings screen
|
|
||||||
if t.currentMode == titleModeSettings {
|
if t.currentMode == titleModeSettings {
|
||||||
t.settingsScreen.Draw(screen, screenWidth, screenHeight, "SETTINGS")
|
t.settingsScreen.Draw(screen, screenWidth, screenHeight, "SETTINGS")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw main menu
|
|
||||||
screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
|
screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
|
||||||
|
|
||||||
// Draw game title
|
|
||||||
titleText := "LIL GUY"
|
titleText := "LIL GUY"
|
||||||
scale := 3.0
|
scale := 3.0
|
||||||
charWidth := 7.0 * scale
|
charWidth := 7.0 * scale
|
||||||
@@ -144,12 +130,11 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
|||||||
op.ColorScale.ScaleWithColor(color.RGBA{R: 210, G: 220, B: 255, A: 255})
|
op.ColorScale.ScaleWithColor(color.RGBA{R: 210, G: 220, B: 255, A: 255})
|
||||||
text.Draw(screen, titleText, basicFace, op)
|
text.Draw(screen, titleText, basicFace, op)
|
||||||
|
|
||||||
// Draw menu options
|
// Draw menu
|
||||||
options := []string{"Continue", "New Game", "Settings", "Quit"}
|
options := []string{"Continue", "New Game", "Settings", "Quit"}
|
||||||
startY := screenHeight/2 + 10
|
startY := screenHeight/2 + 10
|
||||||
|
|
||||||
for i, option := range options {
|
for i, option := range options {
|
||||||
// Skip Continue option if no save exists
|
|
||||||
if i == int(TitleOptionContinue) && !t.hasSaveGame {
|
if i == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -163,12 +148,10 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i == t.selectedIndex {
|
if i == t.selectedIndex {
|
||||||
// Draw selection indicator
|
|
||||||
indicatorX := optionX - 20
|
indicatorX := optionX - 20
|
||||||
t.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, optionY)
|
t.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, optionY)
|
||||||
t.drawText(screen, option, color.RGBA{R: 255, G: 255, B: 100, A: 255}, optionX, optionY)
|
t.drawText(screen, option, color.RGBA{R: 255, G: 255, B: 100, A: 255}, optionX, optionY)
|
||||||
|
|
||||||
// Draw selection box
|
|
||||||
boxPadding := float32(10.0)
|
boxPadding := float32(10.0)
|
||||||
boxWidth := float32(len(option)*7) + boxPadding*2
|
boxWidth := float32(len(option)*7) + boxPadding*2
|
||||||
boxHeight := float32(20)
|
boxHeight := float32(20)
|
||||||
@@ -182,7 +165,7 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw hint text
|
// Instructions
|
||||||
hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
|
hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
|
||||||
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
||||||
hintY := screenHeight - 50
|
hintY := screenHeight - 50
|
||||||
@@ -196,9 +179,7 @@ func (t *TitleScreen) drawText(screen *ebiten.Image, txt string, clr color.Color
|
|||||||
text.Draw(screen, txt, basicFace, op)
|
text.Draw(screen, txt, basicFace, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets the title screen to its initial state.
|
|
||||||
func (t *TitleScreen) Reset() {
|
func (t *TitleScreen) Reset() {
|
||||||
// Start at Continue if available, otherwise New Game
|
|
||||||
if t.hasSaveGame {
|
if t.hasSaveGame {
|
||||||
t.selectedIndex = 0
|
t.selectedIndex = 0
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HUD resource entry.
|
// Meter types
|
||||||
|
|
||||||
type Meter struct {
|
type Meter struct {
|
||||||
Label string
|
Label string
|
||||||
Base float64
|
Base float64
|
||||||
@@ -13,19 +14,18 @@ type Meter struct {
|
|||||||
Color color.NRGBA
|
Color color.NRGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meter template values.
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Label string
|
Label string
|
||||||
Base float64
|
Base float64
|
||||||
Color color.NRGBA
|
Color color.NRGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection of meters.
|
// Meter manager
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
meters []Meter
|
meters []Meter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds meters from configs.
|
|
||||||
func NewManager(cfgs []Config) *Manager {
|
func NewManager(cfgs []Config) *Manager {
|
||||||
meters := make([]Meter, len(cfgs))
|
meters := make([]Meter, len(cfgs))
|
||||||
for i, cfg := range cfgs {
|
for i, cfg := range cfgs {
|
||||||
@@ -40,20 +40,20 @@ func NewManager(cfgs []Config) *Manager {
|
|||||||
return &Manager{meters: meters}
|
return &Manager{meters: meters}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets levels to base.
|
|
||||||
func (m *Manager) Update() {
|
func (m *Manager) Update() {
|
||||||
for i := range m.meters {
|
for i := range m.meters {
|
||||||
m.meters[i].Level = m.meters[i].Base
|
m.meters[i].Level = m.meters[i].Base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meters exposes a copy of the internal slice to prevent mutation.
|
|
||||||
func (m *Manager) Meters() []Meter {
|
func (m *Manager) Meters() []Meter {
|
||||||
out := make([]Meter, len(m.meters))
|
out := make([]Meter, len(m.meters))
|
||||||
copy(out, m.meters)
|
copy(out, m.meters)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
func clamp(value, min, max float64) float64 {
|
func clamp(value, min, max float64) float64 {
|
||||||
if value < min {
|
if value < min {
|
||||||
return min
|
return min
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ var (
|
|||||||
rectPixel = newRectPixel()
|
rectPixel = newRectPixel()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Drawing helpers
|
||||||
|
|
||||||
func newRectPixel() *ebiten.Image {
|
func newRectPixel() *ebiten.Image {
|
||||||
img := ebiten.NewImage(1, 1)
|
img := ebiten.NewImage(1, 1)
|
||||||
img.Fill(color.White)
|
img.Fill(color.White)
|
||||||
@@ -33,12 +35,12 @@ func drawHUDText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
|
|||||||
text.Draw(screen, txt, basicFace, op)
|
text.Draw(screen, txt, basicFace, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drawable HUD chunk.
|
// HUD elements
|
||||||
|
|
||||||
type Element interface {
|
type Element interface {
|
||||||
Draw(screen *ebiten.Image, x, y int) (width, height int)
|
Draw(screen *ebiten.Image, x, y int) (width, height int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain text node.
|
|
||||||
type Label struct {
|
type Label struct {
|
||||||
Text string
|
Text string
|
||||||
Color color.Color
|
Color color.Color
|
||||||
@@ -53,7 +55,6 @@ func (l Label) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
return width, 13
|
return width, 13
|
||||||
}
|
}
|
||||||
|
|
||||||
// Percent readout node.
|
|
||||||
type PercentageDisplay struct {
|
type PercentageDisplay struct {
|
||||||
Meter status.Meter
|
Meter status.Meter
|
||||||
Color color.Color
|
Color color.Color
|
||||||
@@ -68,7 +69,6 @@ func (p PercentageDisplay) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
return len(txt) * 7, 13
|
return len(txt) * 7, 13
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined label and percent.
|
|
||||||
type MeterLabel struct {
|
type MeterLabel struct {
|
||||||
Meter status.Meter
|
Meter status.Meter
|
||||||
Color color.Color
|
Color color.Color
|
||||||
@@ -80,17 +80,14 @@ func (m MeterLabel) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
}
|
}
|
||||||
var txt string
|
var txt string
|
||||||
if m.Meter.Base < 0 {
|
if m.Meter.Base < 0 {
|
||||||
// Text-only display without percentage.
|
|
||||||
txt = m.Meter.Label
|
txt = m.Meter.Label
|
||||||
} else {
|
} else {
|
||||||
// Standard meter with percentage.
|
|
||||||
txt = fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level)
|
txt = fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level)
|
||||||
}
|
}
|
||||||
drawHUDText(screen, txt, m.Meter.Color, x, y)
|
drawHUDText(screen, txt, m.Meter.Color, x, y)
|
||||||
return len(txt) * 7, 13
|
return len(txt) * 7, 13
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal meter bar.
|
|
||||||
type Bar struct {
|
type Bar struct {
|
||||||
Meter status.Meter
|
Meter status.Meter
|
||||||
MaxWidth int
|
MaxWidth int
|
||||||
@@ -117,23 +114,17 @@ func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
fillWidth = maxWidth
|
fillWidth = maxWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw border if requested
|
|
||||||
if b.ShowBorder {
|
if b.ShowBorder {
|
||||||
borderColor := b.BorderColor
|
borderColor := b.BorderColor
|
||||||
if borderColor == nil {
|
if borderColor == nil {
|
||||||
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
|
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
|
||||||
}
|
}
|
||||||
// Top border
|
|
||||||
drawRect(screen, x, y, maxWidth, 1, borderColor)
|
drawRect(screen, x, y, maxWidth, 1, borderColor)
|
||||||
// Bottom border
|
|
||||||
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
|
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
|
||||||
// Left border
|
|
||||||
drawRect(screen, x, y, 1, height, borderColor)
|
drawRect(screen, x, y, 1, height, borderColor)
|
||||||
// Right border
|
|
||||||
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
|
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw filled portion
|
|
||||||
if fillWidth > 0 {
|
if fillWidth > 0 {
|
||||||
drawRect(screen, x, y, fillWidth, height, b.Meter.Color)
|
drawRect(screen, x, y, fillWidth, height, b.Meter.Color)
|
||||||
}
|
}
|
||||||
@@ -141,7 +132,8 @@ func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
return maxWidth, height
|
return maxWidth, height
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for filled rectangles.
|
// Rectangle drawing
|
||||||
|
|
||||||
func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
|
func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return
|
return
|
||||||
@@ -158,7 +150,8 @@ func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
|
|||||||
screen.DrawImage(rectPixel, op)
|
screen.DrawImage(rectPixel, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty padding block.
|
// Layout elements
|
||||||
|
|
||||||
type Spacer struct {
|
type Spacer struct {
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
@@ -168,7 +161,6 @@ func (s Spacer) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
return s.Width, s.Height
|
return s.Width, s.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal layout row.
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
Elements []Element
|
Elements []Element
|
||||||
Spacing int
|
Spacing int
|
||||||
@@ -195,7 +187,6 @@ func (r Row) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
|||||||
return totalWidth, maxHeight
|
return totalWidth, maxHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical stack layout.
|
|
||||||
type Column struct {
|
type Column struct {
|
||||||
Elements []Element
|
Elements []Element
|
||||||
Spacing int
|
Spacing int
|
||||||
|
|||||||
@@ -8,20 +8,17 @@ import (
|
|||||||
"github.com/atridad/LilGuy/internal/status"
|
"github.com/atridad/LilGuy/internal/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HUD overlay anchor.
|
|
||||||
type Overlay struct {
|
type Overlay struct {
|
||||||
X int
|
X int
|
||||||
Y int
|
Y int
|
||||||
Color color.Color
|
Color color.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paints the HUD overlay.
|
|
||||||
func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||||
if o.Color == nil {
|
if o.Color == nil {
|
||||||
o.Color = color.White
|
o.Color = color.White
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instruction text
|
|
||||||
instructions := Column{
|
instructions := Column{
|
||||||
Elements: []Element{
|
Elements: []Element{
|
||||||
Label{Text: "Lil Guy", Color: o.Color},
|
Label{Text: "Lil Guy", Color: o.Color},
|
||||||
@@ -31,16 +28,13 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
|||||||
}
|
}
|
||||||
instructions.Draw(screen, 16, 16)
|
instructions.Draw(screen, 16, 16)
|
||||||
|
|
||||||
// Meter column
|
|
||||||
meterElements := make([]Element, 0, len(meters))
|
meterElements := make([]Element, 0, len(meters))
|
||||||
for _, meter := range meters {
|
for _, meter := range meters {
|
||||||
if meter.Base < 0 {
|
if meter.Base < 0 {
|
||||||
// Text-only display (no bar).
|
|
||||||
meterElements = append(meterElements,
|
meterElements = append(meterElements,
|
||||||
MeterLabel{Meter: meter, Color: o.Color},
|
MeterLabel{Meter: meter, Color: o.Color},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Full meter with bar.
|
|
||||||
meterElements = append(meterElements, Column{
|
meterElements = append(meterElements, Column{
|
||||||
Elements: []Element{
|
Elements: []Element{
|
||||||
MeterLabel{Meter: meter, Color: o.Color},
|
MeterLabel{Meter: meter, Color: o.Color},
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Menu options
|
||||||
|
|
||||||
func getOverlayImage(width, height int) *ebiten.Image {
|
func getOverlayImage(width, height int) *ebiten.Image {
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -46,6 +48,8 @@ const (
|
|||||||
optionCount
|
optionCount
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Screen modes
|
||||||
|
|
||||||
type menuScreen int
|
type menuScreen int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -80,7 +84,8 @@ func (m *PauseMenu) SetFPSCap(cap FPSCapSetting) {
|
|||||||
m.settingsScreen.SetFPSCap(cap)
|
m.settingsScreen.SetFPSCap(cap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the selected option if one was chosen, nil otherwise
|
// Update logic
|
||||||
|
|
||||||
func (m *PauseMenu) Update() *MenuOption {
|
func (m *PauseMenu) Update() *MenuOption {
|
||||||
if m.currentScreen == screenSettings {
|
if m.currentScreen == screenSettings {
|
||||||
return m.updateSettings()
|
return m.updateSettings()
|
||||||
@@ -123,6 +128,8 @@ func (m *PauseMenu) updateSettings() *MenuOption {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
|
||||||
func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||||
if overlay := getOverlayImage(screenWidth, screenHeight); overlay != nil {
|
if overlay := getOverlayImage(screenWidth, screenHeight); overlay != nil {
|
||||||
screen.DrawImage(overlay, nil)
|
screen.DrawImage(overlay, nil)
|
||||||
@@ -177,6 +184,7 @@ func (m *PauseMenu) drawMain(screen *ebiten.Image, menuX, menuY, menuWidth, menu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instructions
|
||||||
hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
|
hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
|
||||||
hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2)
|
hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2)
|
||||||
hintY := menuY + menuHeight - 30
|
hintY := menuY + menuHeight - 30
|
||||||
@@ -184,7 +192,6 @@ func (m *PauseMenu) drawMain(screen *ebiten.Image, menuX, menuY, menuWidth, menu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) {
|
func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) {
|
||||||
// Draw menu background and border
|
|
||||||
vector.DrawFilledRect(screen,
|
vector.DrawFilledRect(screen,
|
||||||
float32(menuX), float32(menuY),
|
float32(menuX), float32(menuY),
|
||||||
float32(menuWidth), float32(menuHeight),
|
float32(menuWidth), float32(menuHeight),
|
||||||
@@ -200,16 +207,12 @@ func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth,
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a sub-image for the settings screen to draw within the menu bounds
|
|
||||||
// We'll draw to the full screen and the settings screen will handle positioning
|
|
||||||
screenWidth := menuWidth
|
screenWidth := menuWidth
|
||||||
screenHeight := menuHeight
|
screenHeight := menuHeight
|
||||||
|
|
||||||
// Temporarily adjust the drawing to center within the menu
|
|
||||||
subScreen := ebiten.NewImage(screenWidth, screenHeight)
|
subScreen := ebiten.NewImage(screenWidth, screenHeight)
|
||||||
m.settingsScreen.Draw(subScreen, screenWidth, screenHeight, "SETTINGS")
|
m.settingsScreen.Draw(subScreen, screenWidth, screenHeight, "SETTINGS")
|
||||||
|
|
||||||
// Draw the settings content in the menu area
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.GeoM.Translate(float64(menuX), float64(menuY))
|
op.GeoM.Translate(float64(menuX), float64(menuY))
|
||||||
screen.DrawImage(subScreen, op)
|
screen.DrawImage(subScreen, op)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Surface types
|
||||||
|
|
||||||
type SurfaceTag int
|
type SurfaceTag int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -67,6 +69,8 @@ func (s *Surface) Draw(screen *ebiten.Image) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// World container
|
||||||
|
|
||||||
type World struct {
|
type World struct {
|
||||||
Surfaces []*Surface
|
Surfaces []*Surface
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user