Consolodated config, added portals, etc.
This commit is contained in:
80
internal/config/config.go
Normal file
80
internal/config/config.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package config
|
||||
|
||||
import "image/color"
|
||||
|
||||
const (
|
||||
ScreenWidth = 960
|
||||
ScreenHeight = 540
|
||||
)
|
||||
|
||||
type GameConfig struct {
|
||||
Screen ScreenConfig
|
||||
Hero HeroConfig
|
||||
HUD HUDConfig
|
||||
Visual VisualConfig
|
||||
}
|
||||
|
||||
type ScreenConfig struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type HeroConfig struct {
|
||||
StartX float64
|
||||
StartY float64
|
||||
Radius float64
|
||||
Speed float64
|
||||
MaxStamina float64
|
||||
StaminaDrain float64
|
||||
StaminaRegen float64
|
||||
Color color.NRGBA
|
||||
}
|
||||
|
||||
type HUDConfig struct {
|
||||
X int
|
||||
Y int
|
||||
Margin int
|
||||
}
|
||||
|
||||
type VisualConfig struct {
|
||||
BackgroundColor color.NRGBA
|
||||
SaveNotificationColor color.NRGBA
|
||||
StaminaNormalColor color.NRGBA
|
||||
StaminaLowColor color.NRGBA
|
||||
FPSGoodColor color.NRGBA
|
||||
FPSWarnColor color.NRGBA
|
||||
FPSPoorColor color.NRGBA
|
||||
}
|
||||
|
||||
func Default() GameConfig {
|
||||
return GameConfig{
|
||||
Screen: ScreenConfig{
|
||||
Width: ScreenWidth,
|
||||
Height: ScreenHeight,
|
||||
},
|
||||
Hero: HeroConfig{
|
||||
StartX: ScreenWidth / 2,
|
||||
StartY: ScreenHeight / 2,
|
||||
Radius: 28.0,
|
||||
Speed: 180.0,
|
||||
MaxStamina: 100.0,
|
||||
StaminaDrain: 50.0,
|
||||
StaminaRegen: 30.0,
|
||||
Color: color.NRGBA{R: 210, G: 220, B: 255, A: 255},
|
||||
},
|
||||
HUD: HUDConfig{
|
||||
X: ScreenWidth - 220,
|
||||
Y: 20,
|
||||
Margin: 16,
|
||||
},
|
||||
Visual: VisualConfig{
|
||||
BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255},
|
||||
SaveNotificationColor: color.NRGBA{R: 50, G: 200, B: 50, A: 255},
|
||||
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},
|
||||
},
|
||||
}
|
||||
}
|
||||
60
internal/config/constants.go
Normal file
60
internal/config/constants.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Animation speeds
|
||||
const (
|
||||
NormalAnimSpeed = 0.03
|
||||
IdleAnimSpeed = 0.1
|
||||
SprintAnimSpeed = 0.01
|
||||
JumpingAnimSpeed = 0.005
|
||||
)
|
||||
|
||||
// Physics
|
||||
const (
|
||||
Gravity = 1200.0
|
||||
JumpStrength = -450.0
|
||||
MaxFallSpeed = 800.0
|
||||
GroundFriction = 0.85
|
||||
AirFriction = 0.95
|
||||
)
|
||||
|
||||
// Gameplay
|
||||
const (
|
||||
SprintSpeedMultiplier = 1.8
|
||||
SprintRecoveryThreshold = 0.2
|
||||
ExhaustedThreshold = 0.2
|
||||
StaminaLowThreshold = 0.2
|
||||
)
|
||||
|
||||
// FPS Monitoring
|
||||
const (
|
||||
FPSWarnThreshold = 0.85
|
||||
FPSPoorThreshold = 0.6
|
||||
FPSSampleWindow = time.Second
|
||||
TargetTPS = 60
|
||||
)
|
||||
|
||||
// Sprite
|
||||
const (
|
||||
AnimFrameWrap = 4096
|
||||
HeroSpriteScale = 0.175
|
||||
FixedSpriteHeight = 329.0
|
||||
FixedSpriteWidth = 315.0
|
||||
)
|
||||
|
||||
// Notifications
|
||||
const (
|
||||
SaveNotificationDuration = 2 * time.Second
|
||||
)
|
||||
|
||||
// Portal
|
||||
const (
|
||||
PortalThickness = 20.0
|
||||
PortalTransitionCooldown = 0.5
|
||||
)
|
||||
|
||||
// World
|
||||
const (
|
||||
GroundHeight = 16.0
|
||||
)
|
||||
78
internal/config/factory.go
Normal file
78
internal/config/factory.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
type MapConfig struct {
|
||||
ID string
|
||||
Number int
|
||||
GroundColor color.NRGBA
|
||||
BackgroundColor color.NRGBA
|
||||
}
|
||||
|
||||
func DefaultMap1() MapConfig {
|
||||
return MapConfig{
|
||||
ID: "map1",
|
||||
Number: 1,
|
||||
GroundColor: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
||||
BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255},
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultMap2() MapConfig {
|
||||
return MapConfig{
|
||||
ID: "map2",
|
||||
Number: 2,
|
||||
GroundColor: color.NRGBA{R: 139, G: 69, B: 19, A: 255},
|
||||
BackgroundColor: color.NRGBA{R: 155, G: 196, B: 215, A: 255},
|
||||
}
|
||||
}
|
||||
|
||||
type PortalConfig struct {
|
||||
ID string
|
||||
DestinationMap string
|
||||
DestinationPortal string
|
||||
Color color.NRGBA
|
||||
GlowColor color.NRGBA
|
||||
}
|
||||
|
||||
func LeftPortalMap1() PortalConfig {
|
||||
return PortalConfig{
|
||||
ID: "map1_left",
|
||||
DestinationMap: "map2",
|
||||
DestinationPortal: "map2_right",
|
||||
Color: color.NRGBA{R: 255, G: 100, B: 100, A: 180},
|
||||
GlowColor: color.NRGBA{R: 255, G: 150, B: 150, A: 100},
|
||||
}
|
||||
}
|
||||
|
||||
func RightPortalMap1() PortalConfig {
|
||||
return PortalConfig{
|
||||
ID: "map1_right",
|
||||
DestinationMap: "map2",
|
||||
DestinationPortal: "map2_left",
|
||||
Color: color.NRGBA{R: 100, G: 255, B: 100, A: 180},
|
||||
GlowColor: color.NRGBA{R: 150, G: 255, B: 150, A: 100},
|
||||
}
|
||||
}
|
||||
|
||||
func LeftPortalMap2() PortalConfig {
|
||||
return PortalConfig{
|
||||
ID: "map2_left",
|
||||
DestinationMap: "map1",
|
||||
DestinationPortal: "map1_right",
|
||||
Color: color.NRGBA{R: 100, G: 255, B: 100, A: 180},
|
||||
GlowColor: color.NRGBA{R: 150, G: 255, B: 150, A: 100},
|
||||
}
|
||||
}
|
||||
|
||||
func RightPortalMap2() PortalConfig {
|
||||
return PortalConfig{
|
||||
ID: "map2_right",
|
||||
DestinationMap: "map1",
|
||||
DestinationPortal: "map1_left",
|
||||
Color: color.NRGBA{R: 255, G: 100, B: 100, A: 180},
|
||||
GlowColor: color.NRGBA{R: 255, G: 150, B: 150, A: 100},
|
||||
}
|
||||
}
|
||||
@@ -101,9 +101,10 @@ type state struct {
|
||||
gameplayScreen *screens.GameplayScreen
|
||||
pauseMenu *menu.PauseMenu
|
||||
|
||||
fpsEnabled bool
|
||||
fpsCap FPSCap
|
||||
saveManager *save.Manager
|
||||
fpsEnabled bool
|
||||
fpsCap FPSCap
|
||||
portalVisibility bool
|
||||
saveManager *save.Manager
|
||||
|
||||
lastAutoSave time.Time
|
||||
autoSaveInterval time.Duration
|
||||
@@ -120,6 +121,7 @@ func newState() *state {
|
||||
lastTick: now,
|
||||
fpsEnabled: false,
|
||||
fpsCap: FPSCap60,
|
||||
portalVisibility: false,
|
||||
lastAutoSave: now,
|
||||
autoSaveInterval: 30 * time.Second,
|
||||
}
|
||||
@@ -133,6 +135,7 @@ func newState() *state {
|
||||
if saveManager != nil {
|
||||
if settings, err := saveManager.LoadSettings(); err == nil {
|
||||
s.fpsEnabled = settings.FPSMonitor
|
||||
s.portalVisibility = settings.PortalVisibility
|
||||
switch settings.FPSCap {
|
||||
case "60":
|
||||
s.fpsCap = FPSCap60
|
||||
@@ -149,14 +152,16 @@ func newState() *state {
|
||||
// Initialize screens
|
||||
s.splashScreen = screens.NewSplashScreen()
|
||||
s.titleScreen = screens.NewTitleScreen()
|
||||
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled)
|
||||
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled, &s.portalVisibility)
|
||||
s.pauseMenu = menu.NewPauseMenu()
|
||||
|
||||
// Wire up settings references
|
||||
s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
|
||||
s.titleScreen.SetFPSCap(&s.fpsCap)
|
||||
s.titleScreen.SetPortalVisibility(&s.portalVisibility)
|
||||
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
|
||||
s.pauseMenu.SetFPSCap(&s.fpsCap)
|
||||
s.pauseMenu.SetPortalVisibility(&s.portalVisibility)
|
||||
|
||||
if saveManager != nil {
|
||||
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
|
||||
@@ -167,8 +172,9 @@ func newState() *state {
|
||||
// Create initial save file
|
||||
if saveManager != nil {
|
||||
settings := &save.Settings{
|
||||
FPSMonitor: s.fpsEnabled,
|
||||
FPSCap: s.fpCapToStringHelper(s.fpsCap),
|
||||
FPSMonitor: s.fpsEnabled,
|
||||
FPSCap: s.fpCapToStringHelper(s.fpsCap),
|
||||
PortalVisibility: s.portalVisibility,
|
||||
}
|
||||
saveManager.SaveSettings(settings)
|
||||
}
|
||||
@@ -192,6 +198,7 @@ func (s *state) fpCapToStringHelper(cap FPSCap) string {
|
||||
func (g *Game) Update() error {
|
||||
prevFPSEnabled := g.state.fpsEnabled
|
||||
prevFPSCap := g.state.fpsCap
|
||||
prevPortalVisibility := g.state.portalVisibility
|
||||
|
||||
currentTPS := g.state.fpsCap.TPS()
|
||||
if currentTPS < 0 {
|
||||
@@ -213,7 +220,7 @@ func (g *Game) Update() error {
|
||||
err = g.updatePaused()
|
||||
}
|
||||
|
||||
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap {
|
||||
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap || prevPortalVisibility != g.state.portalVisibility {
|
||||
g.saveSettings()
|
||||
}
|
||||
|
||||
@@ -338,8 +345,9 @@ func (g *Game) saveSettings() {
|
||||
}
|
||||
|
||||
settings := &save.Settings{
|
||||
FPSMonitor: g.state.fpsEnabled,
|
||||
FPSCap: g.fpCapToString(g.state.fpsCap),
|
||||
FPSMonitor: g.state.fpsEnabled,
|
||||
FPSCap: g.fpCapToString(g.state.fpsCap),
|
||||
PortalVisibility: g.state.portalVisibility,
|
||||
}
|
||||
g.state.saveManager.SaveSettings(settings)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/config"
|
||||
"github.com/atridad/LilGuy/internal/projectile"
|
||||
)
|
||||
|
||||
// Hero defaults
|
||||
const (
|
||||
defaultRadius = 24.0
|
||||
defaultSpeed = 200.0
|
||||
@@ -16,25 +16,7 @@ const (
|
||||
defaultStaminaDrain = 50.0
|
||||
defaultStaminaRegen = 30.0
|
||||
|
||||
sprintSpeedMultiplier = 1.8
|
||||
sprintRecoveryThreshold = 0.2
|
||||
|
||||
normalAnimSpeed = 0.15
|
||||
idleAnimSpeed = 0.3
|
||||
sprintAnimSpeed = 0.08
|
||||
|
||||
exhaustedThreshold = 0.2
|
||||
|
||||
animFrameWrap = 4096
|
||||
heroSpriteScale = 0.175
|
||||
fixedSpriteHeight = 329.0
|
||||
fixedSpriteWidth = 315.0
|
||||
|
||||
gravity = 1200.0
|
||||
jumpStrength = -450.0
|
||||
maxFallSpeed = 800.0
|
||||
groundFriction = 0.85
|
||||
airFriction = 0.95
|
||||
heroSpriteScale = 0.175
|
||||
)
|
||||
|
||||
// Input and bounds
|
||||
@@ -163,9 +145,9 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
||||
// Movement and physics
|
||||
|
||||
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||
h.VelocityY += gravity * dt
|
||||
if h.VelocityY > maxFallSpeed {
|
||||
h.VelocityY = maxFallSpeed
|
||||
h.VelocityY += config.Gravity * dt
|
||||
if h.VelocityY > config.MaxFallSpeed {
|
||||
h.VelocityY = config.MaxFallSpeed
|
||||
}
|
||||
|
||||
h.Y += h.VelocityY * dt
|
||||
@@ -180,7 +162,7 @@ func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||
}
|
||||
|
||||
if input.Jump && h.isGrounded {
|
||||
h.VelocityY = jumpStrength
|
||||
h.VelocityY = config.JumpStrength
|
||||
h.isGrounded = false
|
||||
}
|
||||
|
||||
@@ -198,12 +180,12 @@ func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||
|
||||
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && h.isMoving
|
||||
if h.isSprinting {
|
||||
targetVelocityX *= sprintSpeedMultiplier
|
||||
targetVelocityX *= config.SprintSpeedMultiplier
|
||||
}
|
||||
|
||||
friction := groundFriction
|
||||
friction := config.GroundFriction
|
||||
if !h.isGrounded {
|
||||
friction = airFriction
|
||||
friction = config.AirFriction
|
||||
}
|
||||
|
||||
h.VelocityX = h.VelocityX*friction + targetVelocityX*(1-friction)
|
||||
@@ -223,7 +205,7 @@ func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||
func (h *Hero) updateStamina(input Input, dt float64) {
|
||||
if !input.Sprint {
|
||||
h.wasSprintHeld = false
|
||||
if h.Stamina >= h.MaxStamina*sprintRecoveryThreshold {
|
||||
if h.Stamina >= h.MaxStamina*config.SprintRecoveryThreshold {
|
||||
h.canSprint = true
|
||||
}
|
||||
}
|
||||
@@ -259,21 +241,24 @@ func (h *Hero) updateAnimation(dt float64) {
|
||||
}
|
||||
|
||||
if isMoving {
|
||||
animSpeed := normalAnimSpeed * 0.5
|
||||
animSpeed := config.NormalAnimSpeed * 0.5
|
||||
if h.isSprinting {
|
||||
animSpeed = sprintAnimSpeed * 0.5
|
||||
animSpeed = config.SprintAnimSpeed * 0.5
|
||||
}
|
||||
if !h.isGrounded {
|
||||
animSpeed = config.JumpingAnimSpeed * 0.5
|
||||
}
|
||||
h.animTimer += dt
|
||||
frameAdvance := int(h.animTimer / animSpeed)
|
||||
if frameAdvance > 0 {
|
||||
h.animTimer -= animSpeed * float64(frameAdvance)
|
||||
h.animFrame = (h.animFrame + frameAdvance) % animFrameWrap
|
||||
h.animFrame = (h.animFrame + frameAdvance) % config.AnimFrameWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hero) getVisualState() VisualState {
|
||||
if h.Stamina < h.MaxStamina*exhaustedThreshold {
|
||||
if h.Stamina < h.MaxStamina*config.ExhaustedThreshold {
|
||||
return StateExhausted
|
||||
}
|
||||
|
||||
@@ -296,7 +281,7 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(-actualWidth/2, -actualHeight)
|
||||
op.GeoM.Scale(heroSpriteScale, heroSpriteScale)
|
||||
op.GeoM.Scale(config.HeroSpriteScale, config.HeroSpriteScale)
|
||||
op.GeoM.Translate(h.X, h.Y)
|
||||
|
||||
state := h.getVisualState()
|
||||
|
||||
79
internal/maps/map.go
Normal file
79
internal/maps/map.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/config"
|
||||
"github.com/atridad/LilGuy/internal/portal"
|
||||
"github.com/atridad/LilGuy/internal/world"
|
||||
)
|
||||
|
||||
type Map struct {
|
||||
ID string
|
||||
Number int
|
||||
Width float64
|
||||
Height float64
|
||||
World *world.World
|
||||
Portals []*portal.Portal
|
||||
BackgroundColor color.NRGBA
|
||||
}
|
||||
|
||||
func NewMap(id string, number int, width, height float64) *Map {
|
||||
return &Map{
|
||||
ID: id,
|
||||
Number: number,
|
||||
Width: width,
|
||||
Height: height,
|
||||
World: world.NewWorld(),
|
||||
Portals: make([]*portal.Portal, 0),
|
||||
BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) AddPortal(p *portal.Portal) {
|
||||
m.Portals = append(m.Portals, p)
|
||||
}
|
||||
|
||||
func (m *Map) GetPortalByID(id string) *portal.Portal {
|
||||
for _, p := range m.Portals {
|
||||
if p.ID == id {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateDefaultMaps(screenWidth, screenHeight float64) (*Map, *Map) {
|
||||
map1 := createMapFromConfig(config.DefaultMap1(), screenWidth, screenHeight)
|
||||
map2 := createMapFromConfig(config.DefaultMap2(), screenWidth, screenHeight)
|
||||
|
||||
addPortalFromConfig(map1, config.LeftPortalMap1(), portal.SideLeft, screenWidth, screenHeight)
|
||||
addPortalFromConfig(map1, config.RightPortalMap1(), portal.SideRight, screenWidth, screenHeight)
|
||||
addPortalFromConfig(map2, config.LeftPortalMap2(), portal.SideLeft, screenWidth, screenHeight)
|
||||
addPortalFromConfig(map2, config.RightPortalMap2(), portal.SideRight, screenWidth, screenHeight)
|
||||
|
||||
return map1, map2
|
||||
}
|
||||
|
||||
func createMapFromConfig(cfg config.MapConfig, width, height float64) *Map {
|
||||
m := NewMap(cfg.ID, cfg.Number, width, height)
|
||||
m.BackgroundColor = cfg.BackgroundColor
|
||||
m.World.AddSurface(&world.Surface{
|
||||
X: 0,
|
||||
Y: height - config.GroundHeight,
|
||||
Width: width,
|
||||
Height: config.GroundHeight,
|
||||
Tag: world.TagGround,
|
||||
Color: cfg.GroundColor,
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func addPortalFromConfig(m *Map, cfg config.PortalConfig, side portal.PortalSide, screenWidth, screenHeight float64) {
|
||||
p := portal.CreateSidePortal(cfg.ID, side, screenWidth, screenHeight)
|
||||
p.DestinationMap = cfg.DestinationMap
|
||||
p.DestinationPortal = cfg.DestinationPortal
|
||||
p.Color = cfg.Color
|
||||
p.GlowColor = cfg.GlowColor
|
||||
m.AddPortal(p)
|
||||
}
|
||||
270
internal/portal/portal.go
Normal file
270
internal/portal/portal.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package portal
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/config"
|
||||
)
|
||||
|
||||
type PortalSide int
|
||||
|
||||
const (
|
||||
SideLeft PortalSide = iota
|
||||
SideRight
|
||||
SideTop
|
||||
SideBottom
|
||||
)
|
||||
|
||||
func (s PortalSide) String() string {
|
||||
switch s {
|
||||
case SideLeft:
|
||||
return "Left"
|
||||
case SideRight:
|
||||
return "Right"
|
||||
case SideTop:
|
||||
return "Top"
|
||||
case SideBottom:
|
||||
return "Bottom"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type Portal struct {
|
||||
ID string
|
||||
X float64
|
||||
Y float64
|
||||
Width float64
|
||||
Height float64
|
||||
Side PortalSide
|
||||
|
||||
DestinationMap string
|
||||
DestinationPortal string
|
||||
|
||||
Color color.NRGBA
|
||||
GlowColor color.NRGBA
|
||||
Enabled bool
|
||||
Visible bool
|
||||
GlowIntensity float64
|
||||
|
||||
SpawnOffsetX float64
|
||||
SpawnOffsetY float64
|
||||
}
|
||||
|
||||
type TransitionEvent struct {
|
||||
Portal *Portal
|
||||
HeroX float64
|
||||
HeroY float64
|
||||
DestinationMap string
|
||||
DestinationPortal string
|
||||
}
|
||||
|
||||
func NewPortal(id string, side PortalSide, x, y, width, height float64) *Portal {
|
||||
return &Portal{
|
||||
ID: id,
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: width,
|
||||
Height: height,
|
||||
Side: side,
|
||||
Color: color.NRGBA{R: 100, G: 100, B: 255, A: 180},
|
||||
GlowColor: color.NRGBA{R: 150, G: 150, B: 255, A: 100},
|
||||
Enabled: true,
|
||||
Visible: true,
|
||||
GlowIntensity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks if a point is within the portal bounds
|
||||
func (p *Portal) Contains(x, y float64) bool {
|
||||
return x >= p.X && x <= p.X+p.Width && y >= p.Y && y <= p.Y+p.Height
|
||||
}
|
||||
|
||||
// Overlaps checks if a rectangular area overlaps with the portal
|
||||
func (p *Portal) Overlaps(x, y, width, height float64) bool {
|
||||
return x+width > p.X && x < p.X+p.Width &&
|
||||
y+height > p.Y && y < p.Y+p.Height
|
||||
}
|
||||
|
||||
func (p *Portal) CanTransition() bool {
|
||||
return p.Enabled && p.DestinationMap != ""
|
||||
}
|
||||
|
||||
func (p *Portal) Update(dt float64) {
|
||||
if !p.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
p.GlowIntensity += dt * 2.0
|
||||
if p.GlowIntensity > 6.28 { // 2*PI
|
||||
p.GlowIntensity -= 6.28
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Portal) Draw(screen *ebiten.Image) {
|
||||
if !p.Visible {
|
||||
return
|
||||
}
|
||||
|
||||
vector.FillRect(
|
||||
screen,
|
||||
float32(p.X),
|
||||
float32(p.Y),
|
||||
float32(p.Width),
|
||||
float32(p.Height),
|
||||
p.Color,
|
||||
false,
|
||||
)
|
||||
|
||||
if p.Enabled {
|
||||
glowAlpha := uint8(float64(p.GlowColor.A) * (0.5 + 0.5*float64(p.GlowIntensity)))
|
||||
glowColor := color.NRGBA{
|
||||
R: p.GlowColor.R,
|
||||
G: p.GlowColor.G,
|
||||
B: p.GlowColor.B,
|
||||
A: glowAlpha,
|
||||
}
|
||||
|
||||
borderWidth := float32(4)
|
||||
vector.StrokeRect(
|
||||
screen,
|
||||
float32(p.X)-borderWidth/2,
|
||||
float32(p.Y)-borderWidth/2,
|
||||
float32(p.Width)+borderWidth,
|
||||
float32(p.Height)+borderWidth,
|
||||
borderWidth,
|
||||
glowColor,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
Portals []*Portal
|
||||
OnTransition func(event TransitionEvent)
|
||||
transitionCooldown float64
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
Portals: make([]*Portal, 0),
|
||||
transitionCooldown: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) AddPortal(portal *Portal) {
|
||||
m.Portals = append(m.Portals, portal)
|
||||
}
|
||||
|
||||
func (m *Manager) GetPortalByID(id string) *Portal {
|
||||
for _, p := range m.Portals {
|
||||
if p.ID == id {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RemovePortal(id string) {
|
||||
for i, p := range m.Portals {
|
||||
if p.ID == id {
|
||||
m.Portals = append(m.Portals[:i], m.Portals[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Clear() {
|
||||
m.Portals = make([]*Portal, 0)
|
||||
}
|
||||
|
||||
func (m *Manager) Update(dt float64) {
|
||||
for _, p := range m.Portals {
|
||||
p.Update(dt)
|
||||
}
|
||||
|
||||
if m.transitionCooldown > 0 {
|
||||
m.transitionCooldown -= dt
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) CheckCollision(x, y, width, height float64) *Portal {
|
||||
if m.transitionCooldown > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range m.Portals {
|
||||
if !p.CanTransition() {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Overlaps(x, y, width, height) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) TriggerTransition(portal *Portal, heroX, heroY float64) {
|
||||
if portal == nil || !portal.CanTransition() {
|
||||
return
|
||||
}
|
||||
|
||||
if m.OnTransition != nil {
|
||||
event := TransitionEvent{
|
||||
Portal: portal,
|
||||
HeroX: heroX,
|
||||
HeroY: heroY,
|
||||
DestinationMap: portal.DestinationMap,
|
||||
DestinationPortal: portal.DestinationPortal,
|
||||
}
|
||||
m.OnTransition(event)
|
||||
}
|
||||
|
||||
m.transitionCooldown = config.PortalTransitionCooldown
|
||||
}
|
||||
|
||||
func (m *Manager) Draw(screen *ebiten.Image) {
|
||||
for _, p := range m.Portals {
|
||||
p.Draw(screen)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSidePortal(id string, side PortalSide, screenWidth, screenHeight float64) *Portal {
|
||||
var x, y, width, height float64
|
||||
|
||||
switch side {
|
||||
case SideLeft:
|
||||
x = 0
|
||||
y = 0
|
||||
width = config.PortalThickness
|
||||
height = screenHeight
|
||||
case SideRight:
|
||||
x = screenWidth - config.PortalThickness
|
||||
y = 0
|
||||
width = config.PortalThickness
|
||||
height = screenHeight
|
||||
case SideTop:
|
||||
x = 0
|
||||
y = 0
|
||||
width = screenWidth
|
||||
height = config.PortalThickness
|
||||
case SideBottom:
|
||||
x = 0
|
||||
y = screenHeight - config.PortalThickness
|
||||
width = screenWidth
|
||||
height = config.PortalThickness
|
||||
}
|
||||
|
||||
return NewPortal(id, side, x, y, width, height)
|
||||
}
|
||||
|
||||
func CreateDoorPortal(id string, side PortalSide, x, y float64) *Portal {
|
||||
const doorWidth = 80.0
|
||||
const doorHeight = 120.0
|
||||
|
||||
return NewPortal(id, side, x, y, doorWidth, doorHeight)
|
||||
}
|
||||
@@ -21,8 +21,9 @@ type Data struct {
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
FPSMonitor bool `toml:"fps_monitor"`
|
||||
FPSCap string `toml:"fps_cap"`
|
||||
FPSMonitor bool `toml:"fps_monitor"`
|
||||
FPSCap string `toml:"fps_cap"`
|
||||
PortalVisibility bool `toml:"portal_visibility"`
|
||||
}
|
||||
|
||||
type GameState struct {
|
||||
@@ -59,8 +60,9 @@ func (m *Manager) LoadData() (*Data, error) {
|
||||
if _, err := os.Stat(m.dataPath); os.IsNotExist(err) {
|
||||
return &Data{
|
||||
Settings: Settings{
|
||||
FPSMonitor: false,
|
||||
FPSCap: "60",
|
||||
FPSMonitor: false,
|
||||
FPSCap: "60",
|
||||
PortalVisibility: false,
|
||||
},
|
||||
GameState: GameState{
|
||||
HasSave: false,
|
||||
@@ -137,8 +139,9 @@ func (m *Manager) SaveGameState(state *GameState) error {
|
||||
if err != nil {
|
||||
data = &Data{
|
||||
Settings: Settings{
|
||||
FPSMonitor: false,
|
||||
FPSCap: "60",
|
||||
FPSMonitor: false,
|
||||
FPSCap: "60",
|
||||
PortalVisibility: false,
|
||||
},
|
||||
GameState: *state,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/config"
|
||||
"github.com/atridad/LilGuy/internal/hero"
|
||||
"github.com/atridad/LilGuy/internal/maps"
|
||||
"github.com/atridad/LilGuy/internal/portal"
|
||||
"github.com/atridad/LilGuy/internal/projectile"
|
||||
"github.com/atridad/LilGuy/internal/save"
|
||||
"github.com/atridad/LilGuy/internal/status"
|
||||
@@ -17,51 +20,6 @@ import (
|
||||
"github.com/atridad/LilGuy/internal/world"
|
||||
)
|
||||
|
||||
// Screen and hero defaults
|
||||
var (
|
||||
backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
|
||||
saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, A: 255}
|
||||
)
|
||||
|
||||
const (
|
||||
heroStartX = 960 / 2
|
||||
heroStartY = 540 / 2
|
||||
heroRadius = 28.0
|
||||
heroSpeed = 180.0
|
||||
heroMaxStamina = 100.0
|
||||
heroStaminaDrain = 50.0
|
||||
heroStaminaRegen = 30.0
|
||||
|
||||
saveNotificationDuration = 2 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
||||
)
|
||||
|
||||
// HUD positioning and colors
|
||||
const (
|
||||
hudX = 960 - 220
|
||||
hudY = 20
|
||||
)
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
// FPS monitoring thresholds
|
||||
const (
|
||||
staminaLowThreshold = 0.2
|
||||
fpsWarnThreshold = 0.85
|
||||
fpsPoorThreshold = 0.6
|
||||
fpsSampleWindow = time.Second
|
||||
targetTPS = 60
|
||||
)
|
||||
|
||||
type GameplayInput struct {
|
||||
Left bool
|
||||
Right bool
|
||||
@@ -75,6 +33,7 @@ type GameplayScreen struct {
|
||||
hud hud.Overlay
|
||||
world *world.World
|
||||
projectiles *projectile.Manager
|
||||
portals *portal.Manager
|
||||
bounds hero.Bounds
|
||||
lastTick time.Time
|
||||
gameStartTime time.Time
|
||||
@@ -86,48 +45,69 @@ type GameplayScreen struct {
|
||||
|
||||
saveNotificationTimer time.Duration
|
||||
showSaveNotification bool
|
||||
|
||||
portalVisibility *bool
|
||||
|
||||
currentMap *maps.Map
|
||||
allMaps map[string]*maps.Map
|
||||
}
|
||||
|
||||
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen {
|
||||
w := world.NewWorld()
|
||||
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool, portalVisibility *bool) *GameplayScreen {
|
||||
cfg := config.Default()
|
||||
map1, map2 := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight))
|
||||
|
||||
groundHeight := 16.0
|
||||
w.AddSurface(&world.Surface{
|
||||
X: 0,
|
||||
Y: float64(screenHeight) - groundHeight,
|
||||
Width: float64(screenWidth),
|
||||
Height: groundHeight,
|
||||
Tag: world.TagGround,
|
||||
Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
||||
})
|
||||
allMaps := make(map[string]*maps.Map)
|
||||
allMaps["map1"] = map1
|
||||
allMaps["map2"] = map2
|
||||
|
||||
return &GameplayScreen{
|
||||
portalMgr := portal.NewManager()
|
||||
|
||||
for _, p := range map1.Portals {
|
||||
portalMgr.AddPortal(p)
|
||||
}
|
||||
|
||||
if portalVisibility != nil && *portalVisibility {
|
||||
for _, p := range portalMgr.Portals {
|
||||
p.Visible = true
|
||||
}
|
||||
}
|
||||
|
||||
gs := &GameplayScreen{
|
||||
hero: hero.New(hero.Config{
|
||||
StartX: heroStartX,
|
||||
StartY: heroStartY,
|
||||
Radius: heroRadius,
|
||||
Speed: heroSpeed,
|
||||
Color: heroColor,
|
||||
MaxStamina: heroMaxStamina,
|
||||
StaminaDrain: heroStaminaDrain,
|
||||
StaminaRegen: heroStaminaRegen,
|
||||
StartX: cfg.Hero.StartX,
|
||||
StartY: cfg.Hero.StartY,
|
||||
Radius: cfg.Hero.Radius,
|
||||
Speed: cfg.Hero.Speed,
|
||||
Color: cfg.Hero.Color,
|
||||
MaxStamina: cfg.Hero.MaxStamina,
|
||||
StaminaDrain: cfg.Hero.StaminaDrain,
|
||||
StaminaRegen: cfg.Hero.StaminaRegen,
|
||||
}),
|
||||
hud: hud.Overlay{
|
||||
X: hudX,
|
||||
Y: hudY,
|
||||
Color: color.White,
|
||||
X: cfg.HUD.X,
|
||||
Y: cfg.HUD.Y,
|
||||
Color: color.White,
|
||||
ScreenName: "Map 1",
|
||||
},
|
||||
world: w,
|
||||
world: map1.World,
|
||||
projectiles: projectile.NewManager(),
|
||||
portals: portalMgr,
|
||||
bounds: hero.Bounds{
|
||||
Width: float64(screenWidth),
|
||||
Height: float64(screenHeight),
|
||||
Ground: float64(screenHeight) - groundHeight,
|
||||
Ground: float64(screenHeight) - config.GroundHeight,
|
||||
},
|
||||
lastTick: time.Now(),
|
||||
gameStartTime: time.Now(),
|
||||
fpsEnabled: fpsEnabled,
|
||||
lastTick: time.Now(),
|
||||
gameStartTime: time.Now(),
|
||||
fpsEnabled: fpsEnabled,
|
||||
portalVisibility: portalVisibility,
|
||||
currentMap: map1,
|
||||
allMaps: allMaps,
|
||||
}
|
||||
|
||||
gs.portals.OnTransition = gs.handlePortalTransition
|
||||
|
||||
return gs
|
||||
}
|
||||
|
||||
func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
||||
@@ -149,6 +129,10 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
||||
}
|
||||
|
||||
g.projectiles.Update(dt, g.bounds.Width, g.bounds.Height)
|
||||
g.portals.Update(dt)
|
||||
|
||||
// Check for portal collisions
|
||||
g.checkPortalCollision()
|
||||
|
||||
g.totalPlayTime += delta
|
||||
|
||||
@@ -162,6 +146,71 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
// Portal collision detection
|
||||
func (g *GameplayScreen) checkPortalCollision() {
|
||||
heroRadius := g.hero.Radius
|
||||
heroX := g.hero.X - heroRadius
|
||||
heroY := g.hero.Y - heroRadius
|
||||
heroWidth := heroRadius * 2
|
||||
heroHeight := heroRadius * 2
|
||||
|
||||
if p := g.portals.CheckCollision(heroX, heroY, heroWidth, heroHeight); p != nil {
|
||||
g.portals.TriggerTransition(p, g.hero.X, g.hero.Y)
|
||||
}
|
||||
|
||||
g.updatePortalVisibility()
|
||||
}
|
||||
|
||||
func (g *GameplayScreen) updatePortalVisibility() {
|
||||
if g.portalVisibility == nil {
|
||||
return
|
||||
}
|
||||
|
||||
visible := *g.portalVisibility
|
||||
for _, p := range g.portals.Portals {
|
||||
p.Visible = visible
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GameplayScreen) handlePortalTransition(event portal.TransitionEvent) {
|
||||
destMap, exists := g.allMaps[event.DestinationMap]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Switch to destination map
|
||||
g.currentMap = destMap
|
||||
g.world = destMap.World
|
||||
|
||||
// Clear and reload portals for new map
|
||||
g.portals.Clear()
|
||||
for _, p := range destMap.Portals {
|
||||
g.portals.AddPortal(p)
|
||||
}
|
||||
|
||||
g.updatePortalVisibility()
|
||||
|
||||
// Find destination portal and position hero
|
||||
destPortal := destMap.GetPortalByID(event.DestinationPortal)
|
||||
if destPortal != nil {
|
||||
// Position hero based on which side they're entering from
|
||||
switch destPortal.Side {
|
||||
case portal.SideLeft:
|
||||
g.hero.X = destPortal.X + destPortal.Width + g.hero.Radius + 10
|
||||
g.hero.Y = event.HeroY
|
||||
case portal.SideRight:
|
||||
g.hero.X = destPortal.X - g.hero.Radius - 10
|
||||
g.hero.Y = event.HeroY
|
||||
case portal.SideTop:
|
||||
g.hero.X = event.HeroX
|
||||
g.hero.Y = destPortal.Y + destPortal.Height + g.hero.Radius + 10
|
||||
case portal.SideBottom:
|
||||
g.hero.X = event.HeroX
|
||||
g.hero.Y = destPortal.Y - g.hero.Radius - 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FPS tracking
|
||||
func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
||||
if g.fpsEnabled == nil || !*g.fpsEnabled {
|
||||
@@ -171,7 +220,7 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
||||
g.fpsAccumulator += delta
|
||||
g.fpsFrames++
|
||||
|
||||
if g.fpsAccumulator >= fpsSampleWindow {
|
||||
if g.fpsAccumulator >= config.FPSSampleWindow {
|
||||
g.fpsValue = float64(g.fpsFrames) / g.fpsAccumulator.Seconds()
|
||||
g.fpsAccumulator = 0
|
||||
g.fpsFrames = 0
|
||||
@@ -180,15 +229,25 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
||||
|
||||
// Rendering
|
||||
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||
screen.Fill(backgroundColor)
|
||||
cfg := config.Default()
|
||||
bgColor := cfg.Visual.BackgroundColor
|
||||
if g.currentMap != nil {
|
||||
bgColor = g.currentMap.BackgroundColor
|
||||
}
|
||||
screen.Fill(bgColor)
|
||||
|
||||
g.world.Draw(screen)
|
||||
g.portals.Draw(screen)
|
||||
g.projectiles.Draw(screen)
|
||||
g.hero.Draw(screen)
|
||||
|
||||
staminaColor := staminaNormalColor
|
||||
if g.hero.Stamina < g.hero.MaxStamina*staminaLowThreshold {
|
||||
staminaColor = staminaLowColor
|
||||
if g.currentMap != nil {
|
||||
g.hud.ScreenName = fmt.Sprintf("Map %d", g.currentMap.Number)
|
||||
}
|
||||
|
||||
staminaColor := cfg.Visual.StaminaNormalColor
|
||||
if g.hero.Stamina < g.hero.MaxStamina*config.StaminaLowThreshold {
|
||||
staminaColor = cfg.Visual.StaminaLowColor
|
||||
}
|
||||
|
||||
staminaMeter := status.Meter{
|
||||
@@ -201,13 +260,13 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||
meters := []status.Meter{staminaMeter}
|
||||
|
||||
if g.fpsEnabled != nil && *g.fpsEnabled {
|
||||
ratio := g.fpsValue / float64(targetTPS)
|
||||
fpsColor := fpsGoodColor
|
||||
ratio := g.fpsValue / float64(config.TargetTPS)
|
||||
fpsColor := cfg.Visual.FPSGoodColor
|
||||
switch {
|
||||
case ratio < fpsPoorThreshold:
|
||||
fpsColor = fpsPoorColor
|
||||
case ratio < fpsWarnThreshold:
|
||||
fpsColor = fpsWarnColor
|
||||
case ratio < config.FPSPoorThreshold:
|
||||
fpsColor = cfg.Visual.FPSPoorColor
|
||||
case ratio < config.FPSWarnThreshold:
|
||||
fpsColor = cfg.Visual.FPSWarnColor
|
||||
}
|
||||
|
||||
fpsMeter := status.Meter{
|
||||
@@ -233,7 +292,8 @@ func (g *GameplayScreen) drawSaveNotification(screen *ebiten.Image) {
|
||||
boxWidth := float32(140)
|
||||
boxHeight := float32(40)
|
||||
|
||||
vector.DrawFilledRect(screen,
|
||||
cfg := config.Default()
|
||||
vector.FillRect(screen,
|
||||
centerX-boxWidth/2,
|
||||
centerY-boxHeight/2,
|
||||
boxWidth,
|
||||
@@ -247,7 +307,7 @@ func (g *GameplayScreen) drawSaveNotification(screen *ebiten.Image) {
|
||||
boxWidth,
|
||||
boxHeight,
|
||||
2,
|
||||
saveNotificationColor,
|
||||
cfg.Visual.SaveNotificationColor,
|
||||
false)
|
||||
|
||||
msg := "Game Saved!"
|
||||
@@ -258,41 +318,43 @@ func (g *GameplayScreen) drawSaveNotification(screen *ebiten.Image) {
|
||||
|
||||
func (g *GameplayScreen) ShowSaveNotification() {
|
||||
g.showSaveNotification = true
|
||||
g.saveNotificationTimer = saveNotificationDuration
|
||||
g.saveNotificationTimer = config.SaveNotificationDuration
|
||||
}
|
||||
|
||||
// State management
|
||||
func (g *GameplayScreen) Reset() {
|
||||
cfg := config.Default()
|
||||
screenWidth := int(g.bounds.Width)
|
||||
screenHeight := int(g.bounds.Height)
|
||||
groundHeight := 16.0
|
||||
|
||||
w := world.NewWorld()
|
||||
w.AddSurface(&world.Surface{
|
||||
X: 0,
|
||||
Y: float64(screenHeight) - groundHeight,
|
||||
Width: float64(screenWidth),
|
||||
Height: groundHeight,
|
||||
Tag: world.TagGround,
|
||||
Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
||||
})
|
||||
map1, map2 := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight))
|
||||
g.allMaps = make(map[string]*maps.Map)
|
||||
g.allMaps["map1"] = map1
|
||||
g.allMaps["map2"] = map2
|
||||
g.currentMap = map1
|
||||
|
||||
g.hero = hero.New(hero.Config{
|
||||
StartX: heroStartX,
|
||||
StartY: heroStartY,
|
||||
Radius: heroRadius,
|
||||
Speed: heroSpeed,
|
||||
Color: heroColor,
|
||||
MaxStamina: heroMaxStamina,
|
||||
StaminaDrain: heroStaminaDrain,
|
||||
StaminaRegen: heroStaminaRegen,
|
||||
StartX: cfg.Hero.StartX,
|
||||
StartY: cfg.Hero.StartY,
|
||||
Radius: cfg.Hero.Radius,
|
||||
Speed: cfg.Hero.Speed,
|
||||
Color: cfg.Hero.Color,
|
||||
MaxStamina: cfg.Hero.MaxStamina,
|
||||
StaminaDrain: cfg.Hero.StaminaDrain,
|
||||
StaminaRegen: cfg.Hero.StaminaRegen,
|
||||
})
|
||||
g.world = w
|
||||
g.world = map1.World
|
||||
g.projectiles = projectile.NewManager()
|
||||
g.portals = portal.NewManager()
|
||||
for _, p := range map1.Portals {
|
||||
g.portals.AddPortal(p)
|
||||
}
|
||||
g.portals.OnTransition = g.handlePortalTransition
|
||||
g.updatePortalVisibility()
|
||||
g.bounds = hero.Bounds{
|
||||
Width: float64(screenWidth),
|
||||
Height: float64(screenHeight),
|
||||
Ground: float64(screenHeight) - groundHeight,
|
||||
Ground: float64(screenHeight) - config.GroundHeight,
|
||||
}
|
||||
g.lastTick = time.Now()
|
||||
g.gameStartTime = time.Now()
|
||||
@@ -312,37 +374,39 @@ func (g *GameplayScreen) SaveState() *save.GameState {
|
||||
}
|
||||
|
||||
func (g *GameplayScreen) LoadState(state *save.GameState) {
|
||||
cfg := config.Default()
|
||||
screenWidth := int(g.bounds.Width)
|
||||
screenHeight := int(g.bounds.Height)
|
||||
groundHeight := 16.0
|
||||
|
||||
w := world.NewWorld()
|
||||
w.AddSurface(&world.Surface{
|
||||
X: 0,
|
||||
Y: float64(screenHeight) - groundHeight,
|
||||
Width: float64(screenWidth),
|
||||
Height: groundHeight,
|
||||
Tag: world.TagGround,
|
||||
Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
||||
})
|
||||
map1, map2 := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight))
|
||||
g.allMaps = make(map[string]*maps.Map)
|
||||
g.allMaps["map1"] = map1
|
||||
g.allMaps["map2"] = map2
|
||||
g.currentMap = map1
|
||||
|
||||
g.hero = hero.New(hero.Config{
|
||||
StartX: state.HeroX,
|
||||
StartY: state.HeroY,
|
||||
Radius: heroRadius,
|
||||
Speed: heroSpeed,
|
||||
Color: heroColor,
|
||||
MaxStamina: heroMaxStamina,
|
||||
StaminaDrain: heroStaminaDrain,
|
||||
StaminaRegen: heroStaminaRegen,
|
||||
Radius: cfg.Hero.Radius,
|
||||
Speed: cfg.Hero.Speed,
|
||||
Color: cfg.Hero.Color,
|
||||
MaxStamina: cfg.Hero.MaxStamina,
|
||||
StaminaDrain: cfg.Hero.StaminaDrain,
|
||||
StaminaRegen: cfg.Hero.StaminaRegen,
|
||||
})
|
||||
g.hero.Stamina = state.HeroStamina
|
||||
g.world = w
|
||||
g.world = map1.World
|
||||
g.projectiles = projectile.NewManager()
|
||||
g.portals = portal.NewManager()
|
||||
for _, p := range map1.Portals {
|
||||
g.portals.AddPortal(p)
|
||||
}
|
||||
g.portals.OnTransition = g.handlePortalTransition
|
||||
g.updatePortalVisibility()
|
||||
g.bounds = hero.Bounds{
|
||||
Width: float64(screenWidth),
|
||||
Height: float64(screenHeight),
|
||||
Ground: float64(screenHeight) - groundHeight,
|
||||
Ground: float64(screenHeight) - config.GroundHeight,
|
||||
}
|
||||
g.totalPlayTime = time.Duration(state.PlayTimeMS) * time.Millisecond
|
||||
g.lastTick = time.Now()
|
||||
|
||||
@@ -15,9 +15,10 @@ type FPSCapSetting interface {
|
||||
}
|
||||
|
||||
type SettingsScreen struct {
|
||||
selectedIndex int
|
||||
fpsMonitorValue *bool
|
||||
fpsCapValue FPSCapSetting
|
||||
selectedIndex int
|
||||
fpsMonitorValue *bool
|
||||
fpsCapValue FPSCapSetting
|
||||
portalVisibilityValue *bool
|
||||
}
|
||||
|
||||
func NewSettingsScreen() *SettingsScreen {
|
||||
@@ -34,12 +35,16 @@ func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) {
|
||||
s.fpsCapValue = cap
|
||||
}
|
||||
|
||||
func (s *SettingsScreen) SetPortalVisibility(enabled *bool) {
|
||||
s.portalVisibilityValue = enabled
|
||||
}
|
||||
|
||||
func (s *SettingsScreen) Update() bool {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
return true
|
||||
}
|
||||
|
||||
settingsCount := 2
|
||||
settingsCount := 3
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
|
||||
s.selectedIndex--
|
||||
if s.selectedIndex < 0 {
|
||||
@@ -58,6 +63,8 @@ func (s *SettingsScreen) Update() bool {
|
||||
*s.fpsMonitorValue = !*s.fpsMonitorValue
|
||||
} else if s.selectedIndex == 1 && s.fpsCapValue != nil {
|
||||
s.fpsCapValue.Cycle()
|
||||
} else if s.selectedIndex == 2 && s.portalVisibilityValue != nil {
|
||||
*s.portalVisibilityValue = !*s.portalVisibilityValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +116,23 @@ 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)
|
||||
}
|
||||
|
||||
// Portal visibility toggle
|
||||
portalVisText := "Portal Visibility: "
|
||||
if s.portalVisibilityValue != nil && *s.portalVisibilityValue {
|
||||
portalVisText += "ON"
|
||||
} else {
|
||||
portalVisText += "OFF"
|
||||
}
|
||||
|
||||
portalY := startY + 80
|
||||
if s.selectedIndex == 2 {
|
||||
indicatorX := leftMargin - 20
|
||||
s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, portalY)
|
||||
s.drawText(screen, portalVisText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, portalY)
|
||||
} else {
|
||||
s.drawText(screen, portalVisText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, portalY)
|
||||
}
|
||||
|
||||
// Instructions
|
||||
hintText := "Enter/Space to toggle, ESC to go back"
|
||||
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
)
|
||||
|
||||
// Menu options
|
||||
@@ -52,6 +51,10 @@ func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) {
|
||||
t.settingsScreen.SetFPSCap(cap)
|
||||
}
|
||||
|
||||
func (t *TitleScreen) SetPortalVisibility(enabled *bool) {
|
||||
t.settingsScreen.SetPortalVisibility(enabled)
|
||||
}
|
||||
|
||||
func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
|
||||
t.hasSaveGame = hasSave
|
||||
if !hasSave && t.selectedIndex == 0 {
|
||||
@@ -151,15 +154,6 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -114,21 +114,24 @@ func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
fillWidth = maxWidth
|
||||
}
|
||||
|
||||
if b.ShowBorder {
|
||||
borderColor := b.BorderColor
|
||||
if borderColor == nil {
|
||||
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
|
||||
}
|
||||
drawRect(screen, x, y, maxWidth, 1, borderColor)
|
||||
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
|
||||
drawRect(screen, x, y, 1, height, borderColor)
|
||||
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
|
||||
}
|
||||
// Draw dark background
|
||||
drawRect(screen, x, y, maxWidth, height, color.RGBA{R: 30, G: 30, B: 30, A: 255})
|
||||
|
||||
// Draw filled portion
|
||||
if fillWidth > 0 {
|
||||
drawRect(screen, x, y, fillWidth, height, b.Meter.Color)
|
||||
}
|
||||
|
||||
// Draw border
|
||||
borderColor := b.BorderColor
|
||||
if borderColor == nil {
|
||||
borderColor = color.RGBA{R: 180, G: 180, B: 180, A: 255}
|
||||
}
|
||||
drawRect(screen, x, y, maxWidth, 1, borderColor)
|
||||
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
|
||||
drawRect(screen, x, y, 1, height, borderColor)
|
||||
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
|
||||
|
||||
return maxWidth, height
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,16 @@ import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/status"
|
||||
)
|
||||
|
||||
type Overlay struct {
|
||||
X int
|
||||
Y int
|
||||
Color color.Color
|
||||
X int
|
||||
Y int
|
||||
Color color.Color
|
||||
ScreenName string
|
||||
}
|
||||
|
||||
func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||
@@ -19,35 +21,64 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||
o.Color = color.White
|
||||
}
|
||||
|
||||
// Draw background box for instructions
|
||||
instrBoxWidth := float32(280)
|
||||
instrBoxHeight := float32(78)
|
||||
instrBoxPaddingX := float32(12)
|
||||
instrBoxPaddingY := float32(15)
|
||||
vector.FillRect(screen, 10, 10, instrBoxWidth, instrBoxHeight,
|
||||
color.NRGBA{R: 0, G: 0, B: 0, A: 180}, false)
|
||||
vector.StrokeRect(screen, 10, 10, instrBoxWidth, instrBoxHeight, 2,
|
||||
color.NRGBA{R: 200, G: 200, B: 200, A: 255}, false)
|
||||
|
||||
screenName := o.ScreenName
|
||||
if screenName == "" {
|
||||
screenName = "Lil Guy"
|
||||
}
|
||||
|
||||
instructions := Column{
|
||||
Elements: []Element{
|
||||
Label{Text: "Lil Guy", Color: o.Color},
|
||||
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
||||
Label{Text: "Hold Shift to Sprint", Color: o.Color}},
|
||||
Label{Text: screenName, Color: color.NRGBA{R: 255, G: 255, B: 100, A: 255}},
|
||||
Label{Text: "Move with Arrow Keys / WASD", Color: color.NRGBA{R: 230, G: 230, B: 230, A: 255}},
|
||||
Label{Text: "Hold Shift to Sprint", Color: color.NRGBA{R: 230, G: 230, B: 230, A: 255}},
|
||||
},
|
||||
Spacing: 7,
|
||||
}
|
||||
instructions.Draw(screen, 16, 16)
|
||||
instructions.Draw(screen, int(10+instrBoxPaddingX), int(10+instrBoxPaddingY))
|
||||
|
||||
// Draw background box for meters
|
||||
meterBoxPaddingX := float32(12)
|
||||
meterBoxPaddingTop := float32(15)
|
||||
meterBoxPaddingBottom := float32(8)
|
||||
meterBoxWidth := float32(220)
|
||||
meterBoxHeight := float32(meterBoxPaddingTop + meterBoxPaddingBottom + float32(len(meters))*35 - 5)
|
||||
meterBoxX := float32(o.X) - meterBoxPaddingX
|
||||
meterBoxY := float32(o.Y) - meterBoxPaddingTop
|
||||
vector.FillRect(screen, meterBoxX, meterBoxY, meterBoxWidth, meterBoxHeight,
|
||||
color.NRGBA{R: 0, G: 0, B: 0, A: 180}, false)
|
||||
vector.StrokeRect(screen, meterBoxX, meterBoxY, meterBoxWidth, meterBoxHeight, 2,
|
||||
color.NRGBA{R: 200, G: 200, B: 200, A: 255}, false)
|
||||
|
||||
meterElements := make([]Element, 0, len(meters))
|
||||
for _, meter := range meters {
|
||||
if meter.Base < 0 {
|
||||
meterElements = append(meterElements,
|
||||
MeterLabel{Meter: meter, Color: o.Color},
|
||||
MeterLabel{Meter: meter, Color: color.NRGBA{R: 255, G: 255, B: 255, A: 255}},
|
||||
)
|
||||
} else {
|
||||
meterElements = append(meterElements, Column{
|
||||
Elements: []Element{
|
||||
MeterLabel{Meter: meter, Color: o.Color},
|
||||
Bar{Meter: meter, MaxWidth: 180, Height: 8, ShowBorder: false},
|
||||
MeterLabel{Meter: meter, Color: color.NRGBA{R: 255, G: 255, B: 255, A: 255}},
|
||||
Bar{Meter: meter, MaxWidth: 180, Height: 10, ShowBorder: true},
|
||||
},
|
||||
Spacing: 2,
|
||||
Spacing: 4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
meterPanel := Column{
|
||||
Elements: meterElements,
|
||||
Spacing: 16,
|
||||
Spacing: 18,
|
||||
}
|
||||
meterPanel.Draw(screen, o.X, o.Y)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ func (m *PauseMenu) SetFPSCap(cap FPSCapSetting) {
|
||||
m.settingsScreen.SetFPSCap(cap)
|
||||
}
|
||||
|
||||
func (m *PauseMenu) SetPortalVisibility(enabled *bool) {
|
||||
m.settingsScreen.SetPortalVisibility(enabled)
|
||||
}
|
||||
|
||||
// Update logic
|
||||
|
||||
func (m *PauseMenu) Update() *MenuOption {
|
||||
@@ -140,7 +144,7 @@ func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||
menuX := (screenWidth - menuWidth) / 2
|
||||
menuY := (screenHeight - menuHeight) / 2
|
||||
|
||||
vector.DrawFilledRect(screen,
|
||||
vector.FillRect(screen,
|
||||
float32(menuX), float32(menuY),
|
||||
float32(menuWidth), float32(menuHeight),
|
||||
color.RGBA{R: 40, G: 40, B: 50, A: 255},
|
||||
@@ -192,7 +196,7 @@ func (m *PauseMenu) drawMain(screen *ebiten.Image, menuX, menuY, menuWidth, menu
|
||||
}
|
||||
|
||||
func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) {
|
||||
vector.DrawFilledRect(screen,
|
||||
vector.FillRect(screen,
|
||||
float32(menuX), float32(menuY),
|
||||
float32(menuWidth), float32(menuHeight),
|
||||
color.RGBA{R: 40, G: 40, B: 50, A: 255},
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *Surface) IsWalkable() bool {
|
||||
}
|
||||
|
||||
func (s *Surface) Draw(screen *ebiten.Image) {
|
||||
vector.DrawFilledRect(
|
||||
vector.FillRect(
|
||||
screen,
|
||||
float32(s.X),
|
||||
float32(s.Y),
|
||||
|
||||
Reference in New Issue
Block a user