Menu and save system
This commit is contained in:
235
internal/screens/gameplay.go
Normal file
235
internal/screens/gameplay.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/hero"
|
||||
"github.com/atridad/BigFeelings/internal/save"
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
"github.com/atridad/BigFeelings/internal/ui/hud"
|
||||
)
|
||||
|
||||
var (
|
||||
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
||||
)
|
||||
|
||||
// Hero settings.
|
||||
const (
|
||||
heroStartX = 960 / 2 // ScreenWidth / 2
|
||||
heroStartY = 540 / 2 // ScreenHeight / 2
|
||||
heroRadius = 28.0
|
||||
heroSpeed = 180.0
|
||||
heroMaxStamina = 100.0
|
||||
heroStaminaDrain = 50.0
|
||||
heroStaminaRegen = 30.0
|
||||
)
|
||||
|
||||
var (
|
||||
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
||||
)
|
||||
|
||||
// HUD settings.
|
||||
const (
|
||||
hudX = 960 - 220 // ScreenWidth - 220
|
||||
hudY = 20
|
||||
)
|
||||
|
||||
// HUD colors.
|
||||
var (
|
||||
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
|
||||
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
|
||||
fpsGoodColor = color.NRGBA{R: 120, G: 255, B: 120, A: 255}
|
||||
fpsWarnColor = color.NRGBA{R: 255, G: 210, B: 100, A: 255}
|
||||
fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255}
|
||||
)
|
||||
|
||||
const (
|
||||
staminaLowThreshold = 0.2
|
||||
fpsWarnThreshold = 0.85
|
||||
fpsPoorThreshold = 0.6
|
||||
fpsSampleWindow = time.Second
|
||||
targetTPS = 60
|
||||
)
|
||||
|
||||
// Player input for the gameplay screen.
|
||||
type GameplayInput struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Sprint 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 {
|
||||
hero *hero.Hero
|
||||
hud hud.Overlay
|
||||
bounds hero.Bounds
|
||||
lastTick time.Time
|
||||
gameStartTime time.Time
|
||||
totalPlayTime time.Duration
|
||||
fpsEnabled *bool
|
||||
fpsFrames int
|
||||
fpsAccumulator time.Duration
|
||||
fpsValue float64
|
||||
}
|
||||
|
||||
// Creates a new gameplay screen instance.
|
||||
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen {
|
||||
return &GameplayScreen{
|
||||
hero: hero.New(hero.Config{
|
||||
StartX: heroStartX,
|
||||
StartY: heroStartY,
|
||||
Radius: heroRadius,
|
||||
Speed: heroSpeed,
|
||||
Color: heroColor,
|
||||
MaxStamina: heroMaxStamina,
|
||||
StaminaDrain: heroStaminaDrain,
|
||||
StaminaRegen: heroStaminaRegen,
|
||||
}),
|
||||
hud: hud.Overlay{
|
||||
X: hudX,
|
||||
Y: hudY,
|
||||
Color: color.White,
|
||||
},
|
||||
bounds: hero.Bounds{
|
||||
Width: float64(screenWidth),
|
||||
Height: float64(screenHeight),
|
||||
},
|
||||
lastTick: time.Now(),
|
||||
gameStartTime: time.Now(),
|
||||
fpsEnabled: fpsEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
// Processes gameplay logic with the given input and delta time.
|
||||
func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
||||
dt := delta.Seconds()
|
||||
|
||||
g.hero.Update(hero.Input{
|
||||
Left: input.Left,
|
||||
Right: input.Right,
|
||||
Up: input.Up,
|
||||
Down: input.Down,
|
||||
Sprint: input.Sprint,
|
||||
}, dt, g.bounds)
|
||||
|
||||
// Track total play time
|
||||
g.totalPlayTime += delta
|
||||
|
||||
g.trackFPS(delta)
|
||||
}
|
||||
|
||||
// Calculates the current FPS if FPS monitoring is enabled.
|
||||
func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
||||
if g.fpsEnabled == nil || !*g.fpsEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
g.fpsAccumulator += delta
|
||||
g.fpsFrames++
|
||||
|
||||
if g.fpsAccumulator >= fpsSampleWindow {
|
||||
g.fpsValue = float64(g.fpsFrames) / g.fpsAccumulator.Seconds()
|
||||
g.fpsAccumulator = 0
|
||||
g.fpsFrames = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Renders the gameplay screen.
|
||||
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||
screen.Fill(backgroundColor)
|
||||
g.hero.Draw(screen)
|
||||
|
||||
staminaColor := staminaNormalColor
|
||||
if g.hero.Stamina < g.hero.MaxStamina*staminaLowThreshold {
|
||||
staminaColor = staminaLowColor
|
||||
}
|
||||
|
||||
staminaMeter := status.Meter{
|
||||
Label: "Stamina",
|
||||
Base: g.hero.MaxStamina,
|
||||
Level: g.hero.Stamina,
|
||||
Color: staminaColor,
|
||||
}
|
||||
|
||||
meters := []status.Meter{staminaMeter}
|
||||
|
||||
if g.fpsEnabled != nil && *g.fpsEnabled {
|
||||
// Color based on target FPS (60).
|
||||
ratio := g.fpsValue / float64(targetTPS)
|
||||
fpsColor := fpsGoodColor
|
||||
switch {
|
||||
case ratio < fpsPoorThreshold:
|
||||
fpsColor = fpsPoorColor
|
||||
case ratio < fpsWarnThreshold:
|
||||
fpsColor = fpsWarnColor
|
||||
}
|
||||
|
||||
fpsMeter := status.Meter{
|
||||
Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue),
|
||||
Base: -1, // Negative base means text-only display.
|
||||
Level: 0,
|
||||
Color: fpsColor,
|
||||
}
|
||||
meters = append(meters, fpsMeter)
|
||||
}
|
||||
|
||||
g.hud.Draw(screen, meters)
|
||||
}
|
||||
|
||||
// Resets the gameplay screen to its initial state.
|
||||
func (g *GameplayScreen) Reset() {
|
||||
g.hero = hero.New(hero.Config{
|
||||
StartX: heroStartX,
|
||||
StartY: heroStartY,
|
||||
Radius: heroRadius,
|
||||
Speed: heroSpeed,
|
||||
Color: heroColor,
|
||||
MaxStamina: heroMaxStamina,
|
||||
StaminaDrain: heroStaminaDrain,
|
||||
StaminaRegen: heroStaminaRegen,
|
||||
})
|
||||
g.lastTick = time.Now()
|
||||
g.gameStartTime = time.Now()
|
||||
g.totalPlayTime = 0
|
||||
g.fpsFrames = 0
|
||||
g.fpsAccumulator = 0
|
||||
g.fpsValue = 0
|
||||
}
|
||||
|
||||
// SaveState converts the current gameplay state to a saveable format.
|
||||
func (g *GameplayScreen) SaveState() *save.GameState {
|
||||
return &save.GameState{
|
||||
HeroX: g.hero.X,
|
||||
HeroY: g.hero.Y,
|
||||
HeroStamina: g.hero.Stamina,
|
||||
PlayTimeMS: g.totalPlayTime.Milliseconds(),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadState restores gameplay state from saved data.
|
||||
func (g *GameplayScreen) LoadState(state *save.GameState) {
|
||||
g.hero = hero.New(hero.Config{
|
||||
StartX: state.HeroX,
|
||||
StartY: state.HeroY,
|
||||
Radius: heroRadius,
|
||||
Speed: heroSpeed,
|
||||
Color: heroColor,
|
||||
MaxStamina: heroMaxStamina,
|
||||
StaminaDrain: heroStaminaDrain,
|
||||
StaminaRegen: heroStaminaRegen,
|
||||
})
|
||||
g.hero.Stamina = state.HeroStamina
|
||||
g.totalPlayTime = time.Duration(state.PlayTimeMS) * time.Millisecond
|
||||
g.lastTick = time.Now()
|
||||
g.gameStartTime = time.Now()
|
||||
g.fpsFrames = 0
|
||||
g.fpsAccumulator = 0
|
||||
g.fpsValue = 0
|
||||
}
|
||||
136
internal/screens/settings.go
Normal file
136
internal/screens/settings.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
)
|
||||
|
||||
// An interface for managing FPS cap settings.
|
||||
type FPSCapSetting interface {
|
||||
String() string
|
||||
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 {
|
||||
selectedIndex int
|
||||
fpsMonitorValue *bool
|
||||
fpsCapValue FPSCapSetting
|
||||
}
|
||||
|
||||
// Creates a new settings screen instance.
|
||||
func NewSettingsScreen() *SettingsScreen {
|
||||
return &SettingsScreen{
|
||||
selectedIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the FPS monitor toggle reference.
|
||||
func (s *SettingsScreen) SetFPSMonitor(enabled *bool) {
|
||||
s.fpsMonitorValue = enabled
|
||||
}
|
||||
|
||||
// Sets the FPS cap setting reference.
|
||||
func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) {
|
||||
s.fpsCapValue = cap
|
||||
}
|
||||
|
||||
// Processes settings screen input.
|
||||
// Returns true if the user wants to go back.
|
||||
func (s *SettingsScreen) Update() bool {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
return true
|
||||
}
|
||||
|
||||
settingsCount := 2
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
|
||||
s.selectedIndex--
|
||||
if s.selectedIndex < 0 {
|
||||
s.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) {
|
||||
s.selectedIndex++
|
||||
if s.selectedIndex >= settingsCount {
|
||||
s.selectedIndex = settingsCount - 1
|
||||
}
|
||||
}
|
||||
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) {
|
||||
if s.selectedIndex == 0 && s.fpsMonitorValue != nil {
|
||||
*s.fpsMonitorValue = !*s.fpsMonitorValue
|
||||
} else if s.selectedIndex == 1 && s.fpsCapValue != nil {
|
||||
s.fpsCapValue.Cycle()
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Renders the settings screen.
|
||||
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})
|
||||
|
||||
// Draw title
|
||||
titleX := (screenWidth / 2) - (len(title) * 7 / 2)
|
||||
titleY := screenHeight/3 - 50
|
||||
s.drawText(screen, title, color.White, titleX, titleY)
|
||||
|
||||
// Draw settings options
|
||||
startY := screenHeight/2 - 20
|
||||
leftMargin := screenWidth/2 - 120
|
||||
|
||||
fpsMonitorText := "FPS Monitor: "
|
||||
if s.fpsMonitorValue != nil && *s.fpsMonitorValue {
|
||||
fpsMonitorText += "ON"
|
||||
} else {
|
||||
fpsMonitorText += "OFF"
|
||||
}
|
||||
|
||||
if s.selectedIndex == 0 {
|
||||
indicatorX := leftMargin - 20
|
||||
s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, startY)
|
||||
s.drawText(screen, fpsMonitorText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, startY)
|
||||
} else {
|
||||
s.drawText(screen, fpsMonitorText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, startY)
|
||||
}
|
||||
|
||||
fpsCapText := "FPS Cap: "
|
||||
if s.fpsCapValue != nil {
|
||||
fpsCapText += s.fpsCapValue.String()
|
||||
} else {
|
||||
fpsCapText += "60 FPS"
|
||||
}
|
||||
|
||||
capY := startY + 40
|
||||
if s.selectedIndex == 1 {
|
||||
indicatorX := leftMargin - 20
|
||||
s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, capY)
|
||||
s.drawText(screen, fpsCapText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, capY)
|
||||
} else {
|
||||
s.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, capY)
|
||||
}
|
||||
|
||||
// Draw hint text
|
||||
hintText := "Enter/Space to toggle, ESC to go back"
|
||||
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
||||
hintY := screenHeight - 50
|
||||
s.drawText(screen, hintText, color.RGBA{R: 120, G: 120, B: 150, A: 255}, hintX, hintY)
|
||||
}
|
||||
|
||||
func (s *SettingsScreen) drawText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent)
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
text.Draw(screen, txt, basicFace, op)
|
||||
}
|
||||
|
||||
// Resets the settings screen to its initial state.
|
||||
func (s *SettingsScreen) Reset() {
|
||||
s.selectedIndex = 0
|
||||
}
|
||||
101
internal/screens/splash.go
Normal file
101
internal/screens/splash.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
)
|
||||
|
||||
var (
|
||||
basicFace = text.NewGoXFace(basicfont.Face7x13)
|
||||
basicFaceAscent = basicFace.Metrics().HAscent
|
||||
)
|
||||
|
||||
const (
|
||||
splashDuration = 2 * time.Second
|
||||
fadeInDuration = 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 {
|
||||
startTime time.Time
|
||||
fadeInEnd time.Time
|
||||
fadeOutStart time.Time
|
||||
endTime time.Time
|
||||
}
|
||||
|
||||
// Creates a new splash screen instance.
|
||||
func NewSplashScreen() *SplashScreen {
|
||||
now := time.Now()
|
||||
return &SplashScreen{
|
||||
startTime: now,
|
||||
fadeInEnd: now.Add(fadeInDuration),
|
||||
fadeOutStart: now.Add(splashDuration - fadeOutDuration),
|
||||
endTime: now.Add(splashDuration),
|
||||
}
|
||||
}
|
||||
|
||||
// Processes splash screen logic.
|
||||
// Returns true when the splash screen should end and transition to the next screen.
|
||||
func (s *SplashScreen) Update() bool {
|
||||
// Return true if splash is complete
|
||||
if time.Now().After(s.endTime) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Allow skipping with any key
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeySpace) ||
|
||||
inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
|
||||
inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Renders the splash screen.
|
||||
func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||
screen.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 255})
|
||||
|
||||
now := time.Now()
|
||||
alpha := 1.0
|
||||
|
||||
// Calculate fade in/out
|
||||
if now.Before(s.fadeInEnd) {
|
||||
elapsed := now.Sub(s.startTime)
|
||||
alpha = float64(elapsed) / float64(fadeInDuration)
|
||||
} else if now.After(s.fadeOutStart) {
|
||||
elapsed := now.Sub(s.fadeOutStart)
|
||||
alpha = 1.0 - (float64(elapsed) / float64(fadeOutDuration))
|
||||
}
|
||||
|
||||
if alpha < 0 {
|
||||
alpha = 0
|
||||
} else if alpha > 1 {
|
||||
alpha = 1
|
||||
}
|
||||
|
||||
// Draw large game title
|
||||
titleText := "BIG FEELINGS"
|
||||
|
||||
// Calculate size for large text (scale up the basic font)
|
||||
scale := 4.0
|
||||
charWidth := 7.0 * scale
|
||||
textWidth := float64(len(titleText)) * charWidth
|
||||
|
||||
x := float64(screenWidth)/2 - textWidth/2
|
||||
y := float64(screenHeight) / 2
|
||||
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(x, y-basicFaceAscent*scale)
|
||||
op.ColorScale.ScaleWithColor(color.White)
|
||||
op.ColorScale.ScaleAlpha(float32(alpha))
|
||||
text.Draw(screen, titleText, basicFace, op)
|
||||
}
|
||||
209
internal/screens/title.go
Normal file
209
internal/screens/title.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
)
|
||||
|
||||
// Represents the options available on the title screen.
|
||||
type TitleMenuOption int
|
||||
|
||||
const (
|
||||
TitleOptionContinue TitleMenuOption = iota
|
||||
TitleOptionNewGame
|
||||
TitleOptionSettings
|
||||
TitleOptionQuit
|
||||
titleOptionCount
|
||||
)
|
||||
|
||||
type titleScreenMode int
|
||||
|
||||
const (
|
||||
titleModeMain titleScreenMode = iota
|
||||
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 {
|
||||
selectedIndex int
|
||||
currentMode titleScreenMode
|
||||
settingsScreen *SettingsScreen
|
||||
hasSaveGame bool
|
||||
}
|
||||
|
||||
// Creates a new title screen instance.
|
||||
func NewTitleScreen() *TitleScreen {
|
||||
return &TitleScreen{
|
||||
selectedIndex: 0,
|
||||
currentMode: titleModeMain,
|
||||
settingsScreen: NewSettingsScreen(),
|
||||
hasSaveGame: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the FPS monitor toggle reference for settings.
|
||||
func (t *TitleScreen) SetFPSMonitor(enabled *bool) {
|
||||
t.settingsScreen.SetFPSMonitor(enabled)
|
||||
}
|
||||
|
||||
// Sets the FPS cap setting reference for settings.
|
||||
func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) {
|
||||
t.settingsScreen.SetFPSCap(cap)
|
||||
}
|
||||
|
||||
// Sets whether a saved game exists.
|
||||
func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
|
||||
t.hasSaveGame = hasSave
|
||||
// If no save game, skip Continue option
|
||||
if !hasSave && t.selectedIndex == 0 {
|
||||
t.selectedIndex = 1 // Move to New Game
|
||||
}
|
||||
}
|
||||
|
||||
// Processes title screen input and returns the selected option if any.
|
||||
func (t *TitleScreen) Update() *TitleMenuOption {
|
||||
// Handle settings screen
|
||||
if t.currentMode == titleModeSettings {
|
||||
if t.settingsScreen.Update() {
|
||||
t.currentMode = titleModeMain
|
||||
t.selectedIndex = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle main menu
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
|
||||
t.selectedIndex--
|
||||
if t.selectedIndex < 0 {
|
||||
t.selectedIndex = int(titleOptionCount) - 1
|
||||
}
|
||||
// Skip Continue if no save exists
|
||||
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||
t.selectedIndex--
|
||||
if t.selectedIndex < 0 {
|
||||
t.selectedIndex = int(titleOptionCount) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) {
|
||||
t.selectedIndex++
|
||||
if t.selectedIndex >= int(titleOptionCount) {
|
||||
t.selectedIndex = 0
|
||||
}
|
||||
// Skip Continue if no save exists
|
||||
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||
t.selectedIndex++
|
||||
if t.selectedIndex >= int(titleOptionCount) {
|
||||
t.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) {
|
||||
selected := TitleMenuOption(t.selectedIndex)
|
||||
if selected == TitleOptionSettings {
|
||||
t.currentMode = titleModeSettings
|
||||
t.settingsScreen.Reset()
|
||||
return nil
|
||||
}
|
||||
return &selected
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renders the title screen.
|
||||
func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||
// If in settings mode, draw settings screen
|
||||
if t.currentMode == titleModeSettings {
|
||||
t.settingsScreen.Draw(screen, screenWidth, screenHeight, "SETTINGS")
|
||||
return
|
||||
}
|
||||
|
||||
// Draw main menu
|
||||
screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
|
||||
|
||||
// Draw game title
|
||||
titleText := "BIG FEELINGS"
|
||||
scale := 3.0
|
||||
charWidth := 7.0 * scale
|
||||
textWidth := float64(len(titleText)) * charWidth
|
||||
|
||||
titleX := float64(screenWidth)/2 - textWidth/2
|
||||
titleY := float64(screenHeight) / 3
|
||||
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(titleX, titleY-basicFaceAscent*scale)
|
||||
op.ColorScale.ScaleWithColor(color.RGBA{R: 210, G: 220, B: 255, A: 255})
|
||||
text.Draw(screen, titleText, basicFace, op)
|
||||
|
||||
// Draw menu options
|
||||
options := []string{"Continue", "New Game", "Settings", "Quit"}
|
||||
startY := screenHeight/2 + 10
|
||||
|
||||
for i, option := range options {
|
||||
// Skip Continue option if no save exists
|
||||
if i == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||
continue
|
||||
}
|
||||
optionY := startY + (i * 50)
|
||||
optionX := (screenWidth / 2) - (len(option) * 7 / 2)
|
||||
|
||||
// Dim Continue option if it exists but is disabled
|
||||
optionColor := color.RGBA{R: 180, G: 180, B: 200, A: 255}
|
||||
if i == int(TitleOptionContinue) && !t.hasSaveGame {
|
||||
optionColor = color.RGBA{R: 80, G: 80, B: 100, A: 255}
|
||||
}
|
||||
|
||||
if i == t.selectedIndex {
|
||||
// Draw selection indicator
|
||||
indicatorX := optionX - 20
|
||||
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)
|
||||
|
||||
// Draw selection box
|
||||
boxPadding := float32(10.0)
|
||||
boxWidth := float32(len(option)*7) + boxPadding*2
|
||||
boxHeight := float32(20)
|
||||
boxX := float32(optionX) - boxPadding
|
||||
boxY := float32(optionY) - float32(basicFaceAscent) - boxPadding/2
|
||||
|
||||
vector.StrokeRect(screen, boxX, boxY, boxWidth, boxHeight, 2,
|
||||
color.RGBA{R: 255, G: 200, B: 0, A: 255}, false)
|
||||
} else {
|
||||
t.drawText(screen, option, optionColor, optionX, optionY)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hint text
|
||||
hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
|
||||
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
||||
hintY := screenHeight - 50
|
||||
t.drawText(screen, hintText, color.RGBA{R: 120, G: 120, B: 150, A: 255}, hintX, hintY)
|
||||
}
|
||||
|
||||
func (t *TitleScreen) drawText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent)
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
text.Draw(screen, txt, basicFace, op)
|
||||
}
|
||||
|
||||
// Resets the title screen to its initial state.
|
||||
func (t *TitleScreen) Reset() {
|
||||
// Start at Continue if available, otherwise New Game
|
||||
if t.hasSaveGame {
|
||||
t.selectedIndex = 0
|
||||
} else {
|
||||
t.selectedIndex = 1
|
||||
}
|
||||
t.currentMode = titleModeMain
|
||||
t.settingsScreen.Reset()
|
||||
}
|
||||
Reference in New Issue
Block a user