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
|
||||
}
|
||||
Reference in New Issue
Block a user