236 lines
5.6 KiB
Go
236 lines
5.6 KiB
Go
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
|
|
}
|