Lighting experiments

This commit is contained in:
2025-12-16 00:44:52 -07:00
parent c70f85abe5
commit de5f47f47b
12 changed files with 664 additions and 17 deletions

16
go.mod
View File

@@ -3,19 +3,19 @@ module github.com/atridad/LilGuy
go 1.25.4 go 1.25.4
require ( require (
github.com/hajimehoshi/ebiten/v2 v2.9.4 github.com/hajimehoshi/ebiten/v2 v2.9.6
golang.org/x/image v0.31.0 golang.org/x/image v0.34.0
) )
require ( require (
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v1.5.0
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.9.0 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/go-text/typesetting v0.3.0 // indirect github.com/go-text/typesetting v0.3.1 // indirect
github.com/jezek/xgb v1.1.1 // indirect github.com/jezek/xgb v1.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.32.0 // indirect
) )

17
go.sum
View File

@@ -6,25 +6,42 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
github.com/go-text/typesetting v0.3.1 h1:ESHfFntFnJOigjEeEiTc3OGXqggC1eSAAqHkG9ZB+yA=
github.com/go-text/typesetting v0.3.1/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/hajimehoshi/bitmapfont/v4 v4.1.0 h1:eE3qa5Do4qhowZVIHjsrX5pYyyPN6sAFWMsO7QREm3U= github.com/hajimehoshi/bitmapfont/v4 v4.1.0 h1:eE3qa5Do4qhowZVIHjsrX5pYyyPN6sAFWMsO7QREm3U=
github.com/hajimehoshi/bitmapfont/v4 v4.1.0/go.mod h1:/PD+aLjAJ0F2UoQx6hkOfXqWN7BkroDUMr5W+IT1dpE= github.com/hajimehoshi/bitmapfont/v4 v4.1.0/go.mod h1:/PD+aLjAJ0F2UoQx6hkOfXqWN7BkroDUMr5W+IT1dpE=
github.com/hajimehoshi/ebiten/v2 v2.9.4 h1:IlPJpwtksylmmvNhQjv4W2bmCFWXtjY7Z10Esise1bk= github.com/hajimehoshi/ebiten/v2 v2.9.4 h1:IlPJpwtksylmmvNhQjv4W2bmCFWXtjY7Z10Esise1bk=
github.com/hajimehoshi/ebiten/v2 v2.9.4/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM= github.com/hajimehoshi/ebiten/v2 v2.9.4/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
github.com/hajimehoshi/ebiten/v2 v2.9.6 h1:uP41hMkfcbfEfgiTlpzhgnTHGAAfbM/v/pNOZkelI78=
github.com/hajimehoshi/ebiten/v2 v2.9.6/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgb v1.2.0 h1:LzgkD11wOrPnxXEqo588cnjUt4NwMHrFh/tgajo50Q0=
github.com/jezek/xgb v1.2.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=

View File

@@ -6,10 +6,10 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/atridad/LilGuy/internal/maps"
"github.com/atridad/LilGuy/internal/save" "github.com/atridad/LilGuy/internal/save"
"github.com/atridad/LilGuy/internal/screens" "github.com/atridad/LilGuy/internal/screens"
"github.com/atridad/LilGuy/internal/ui/menu" "github.com/atridad/LilGuy/internal/ui/menu"
"github.com/atridad/LilGuy/internal/maps"
) )
const ( const (
@@ -105,6 +105,8 @@ type state struct {
fpsEnabled bool fpsEnabled bool
fpsCap FPSCap fpsCap FPSCap
portalVisibility bool portalVisibility bool
raycastEnabled bool
raycastDebugMode bool
saveManager *save.Manager saveManager *save.Manager
lastAutoSave time.Time lastAutoSave time.Time
@@ -123,6 +125,8 @@ func newState() *state {
fpsEnabled: false, fpsEnabled: false,
fpsCap: FPSCap60, fpsCap: FPSCap60,
portalVisibility: false, portalVisibility: false,
raycastEnabled: true,
raycastDebugMode: false,
lastAutoSave: now, lastAutoSave: now,
autoSaveInterval: 30 * time.Second, autoSaveInterval: 30 * time.Second,
} }
@@ -165,16 +169,18 @@ func newState() *state {
// Initialize 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, mapManager, &s.fpsEnabled, &s.portalVisibility) s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, mapManager, &s.fpsEnabled, &s.portalVisibility, &s.raycastEnabled, &s.raycastDebugMode)
s.pauseMenu = menu.NewPauseMenu() s.pauseMenu = menu.NewPauseMenu()
// Wire up settings references // 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.titleScreen.SetPortalVisibility(&s.portalVisibility) s.titleScreen.SetPortalVisibility(&s.portalVisibility)
s.titleScreen.SetRaycastSettings(&s.raycastEnabled, &s.raycastDebugMode)
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled) s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
s.pauseMenu.SetFPSCap(&s.fpsCap) s.pauseMenu.SetFPSCap(&s.fpsCap)
s.pauseMenu.SetPortalVisibility(&s.portalVisibility) s.pauseMenu.SetPortalVisibility(&s.portalVisibility)
s.pauseMenu.SetRaycastSettings(&s.raycastEnabled, &s.raycastDebugMode)
if saveManager != nil { if saveManager != nil {
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame()) s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
@@ -282,6 +288,16 @@ func (g *Game) updatePlaying() error {
return nil return nil
} }
// Toggle raycasting with L key
if inpututil.IsKeyJustPressed(ebiten.KeyL) {
g.state.gameplayScreen.ToggleRaycast()
}
// Toggle raycasting debug mode with B key
if inpututil.IsKeyJustPressed(ebiten.KeyB) {
g.state.gameplayScreen.ToggleRaycastDebug()
}
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

View File

@@ -90,6 +90,10 @@ type Hero struct {
lastAnimKey animationKey lastAnimKey animationKey
ProjectileConfig projectile.ProjectileConfig ProjectileConfig projectile.ProjectileConfig
// Lighting state
InShadow bool
ShadowIntensity float64
} }
type Config struct { type Config struct {
@@ -358,6 +362,11 @@ func (h *Hero) Draw(screen *ebiten.Image) {
// no color modification for idle // no color modification for idle
} }
if h.InShadow {
darkness := 1.0 - h.ShadowIntensity
op.ColorScale.Scale(float32(darkness), float32(darkness), float32(darkness), 1.0)
}
op.Filter = ebiten.FilterNearest op.Filter = ebiten.FilterNearest
screen.DrawImage(sprite, op) screen.DrawImage(sprite, op)
} }

View File

@@ -10,7 +10,8 @@ import (
func CreateDesert(screenWidth, screenHeight float64) *Map { func CreateDesert(screenWidth, screenHeight float64) *Map {
m := NewMap("desert", 2, "Desert", screenWidth, screenHeight) m := NewMap("desert", 2, "Desert", screenWidth, screenHeight)
m.BackgroundColor = color.NRGBA{R: 155, G: 196, B: 215, A: 255} m.BackgroundColor = color.NRGBA{R: 15, G: 20, B: 40, A: 255} // Dark blue night sky
m.TimeOfDay = Nighttime
// ground surface // ground surface
m.World.AddSurface(&world.Surface{ m.World.AddSurface(&world.Surface{
@@ -22,6 +23,49 @@ func CreateDesert(screenWidth, screenHeight float64) *Map {
Color: color.NRGBA{R: 139, G: 69, B: 19, A: 255}, Color: color.NRGBA{R: 139, G: 69, B: 19, A: 255},
}) })
// Platforms - sandy brown color
platformColor := color.NRGBA{R: 194, G: 134, B: 64, A: 255}
// Platform 1: Low left
m.World.AddSurface(&world.Surface{
X: 120,
Y: screenHeight - 110,
Width: 140,
Height: 20,
Tag: world.TagPlatform,
Color: platformColor,
})
// Platform 2: Mid center-left
m.World.AddSurface(&world.Surface{
X: 320,
Y: screenHeight - 180,
Width: 180,
Height: 20,
Tag: world.TagPlatform,
Color: platformColor,
})
// Platform 3: High center-right
m.World.AddSurface(&world.Surface{
X: 580,
Y: screenHeight - 230,
Width: 160,
Height: 20,
Tag: world.TagPlatform,
Color: platformColor,
})
// Platform 4: Mid right
m.World.AddSurface(&world.Surface{
X: 720,
Y: screenHeight - 140,
Width: 140,
Height: 20,
Tag: world.TagPlatform,
Color: platformColor,
})
// left portal to plains // left portal to plains
leftPortal := portal.CreateSidePortal("desert_left", portal.SideLeft, screenWidth, screenHeight) leftPortal := portal.CreateSidePortal("desert_left", portal.SideLeft, screenWidth, screenHeight)
leftPortal.DestinationMap = "plains" leftPortal.DestinationMap = "plains"

View File

@@ -3,9 +3,16 @@ package maps
import ( import (
"image/color" "image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/atridad/LilGuy/internal/portal" "github.com/atridad/LilGuy/internal/portal"
"github.com/atridad/LilGuy/internal/world" "github.com/atridad/LilGuy/internal/world"
"github.com/hajimehoshi/ebiten/v2"
)
type TimeOfDay string
const (
Daytime TimeOfDay = "day"
Nighttime TimeOfDay = "night"
) )
type Map struct { type Map struct {
@@ -17,6 +24,7 @@ type Map struct {
World *world.World World *world.World
Portals []*portal.Portal Portals []*portal.Portal
BackgroundColor color.NRGBA BackgroundColor color.NRGBA
TimeOfDay TimeOfDay // Day or Night tag
bakedImage *ebiten.Image bakedImage *ebiten.Image
} }
@@ -31,6 +39,7 @@ func NewMap(id string, number int, displayName string, width, height float64) *M
World: world.NewWorld(), World: world.NewWorld(),
Portals: make([]*portal.Portal, 0), Portals: make([]*portal.Portal, 0),
BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255}, BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255},
TimeOfDay: Daytime, // Default to daytime
} }
} }

View File

@@ -11,6 +11,7 @@ import (
func CreatePlains(screenWidth, screenHeight float64) *Map { func CreatePlains(screenWidth, screenHeight float64) *Map {
m := NewMap("plains", 1, "Plains", screenWidth, screenHeight) m := NewMap("plains", 1, "Plains", screenWidth, screenHeight)
m.BackgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255} m.BackgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
m.TimeOfDay = Daytime
// ground surface // ground surface
groundColor := color.NRGBA{R: 34, G: 139, B: 34, A: 255} groundColor := color.NRGBA{R: 34, G: 139, B: 34, A: 255}

353
internal/raycast/raycast.go Normal file
View File

@@ -0,0 +1,353 @@
package raycast
import (
"image/color"
"math"
"sort"
"github.com/atridad/LilGuy/internal/world"
"github.com/hajimehoshi/ebiten/v2"
)
// Line represents a line segment
type Line struct {
X1, Y1, X2, Y2 float64
}
// angle returns the angle of the line
func (l *Line) angle() float64 {
return math.Atan2(l.Y2-l.Y1, l.X2-l.X1)
}
// LightSource represents a light that casts rays
type LightSource struct {
X, Y float64
Radius float64
Color color.RGBA
Intensity float64
}
type System struct {
lights []*LightSource
shadowImage *ebiten.Image
triangleImage *ebiten.Image
screenWidth int
screenHeight int
enabled bool
shadowIntensity float64
}
func NewSystem(screenWidth, screenHeight int) *System {
shadowImage := ebiten.NewImage(screenWidth, screenHeight)
triangleImage := ebiten.NewImage(screenWidth, screenHeight)
triangleImage.Fill(color.White)
return &System{
lights: make([]*LightSource, 0),
shadowImage: shadowImage,
triangleImage: triangleImage,
screenWidth: screenWidth,
screenHeight: screenHeight,
enabled: true,
shadowIntensity: 0.7,
}
}
func (s *System) AddLight(light *LightSource) {
s.lights = append(s.lights, light)
}
func (s *System) ClearLights() {
s.lights = s.lights[:0]
}
func (s *System) SetEnabled(enabled bool) {
s.enabled = enabled
}
func (s *System) IsEnabled() bool {
return s.enabled
}
func (s *System) SetShadowIntensity(intensity float64) {
s.shadowIntensity = intensity
}
func worldToLines(w *world.World) []Line {
var lines []Line
for _, surface := range w.Surfaces {
if !surface.IsSolid() {
continue
}
x, y, width, height := surface.Bounds()
lines = append(lines,
Line{x, y, x + width, y},
Line{x + width, y, x + width, y + height},
Line{x + width, y + height, x, y + height},
Line{x, y + height, x, y},
)
}
return lines
}
// intersection calculates the intersection of two lines
func intersection(l1, l2 Line) (float64, float64, bool) {
denom := (l1.X1-l1.X2)*(l2.Y1-l2.Y2) - (l1.Y1-l1.Y2)*(l2.X1-l2.X2)
tNum := (l1.X1-l2.X1)*(l2.Y1-l2.Y2) - (l1.Y1-l2.Y1)*(l2.X1-l2.X2)
uNum := -((l1.X1-l1.X2)*(l1.Y1-l2.Y1) - (l1.Y1-l1.Y2)*(l1.X1-l2.X1))
if denom == 0 {
return 0, 0, false
}
t := tNum / denom
if t > 1 || t < 0 {
return 0, 0, false
}
u := uNum / denom
if u > 1 || u < 0 {
return 0, 0, false
}
x := l1.X1 + t*(l1.X2-l1.X1)
y := l1.Y1 + t*(l1.Y2-l1.Y1)
return x, y, true
}
// newRay creates a ray from a point at a given angle
func newRay(x, y, length, angle float64) Line {
return Line{
X1: x,
Y1: y,
X2: x + length*math.Cos(angle),
Y2: y + length*math.Sin(angle),
}
}
// getLinePoints extracts unique points from lines
func getLinePoints(lines []Line) [][2]float64 {
pointMap := make(map[[2]float64]bool)
for _, line := range lines {
pointMap[[2]float64{line.X1, line.Y1}] = true
pointMap[[2]float64{line.X2, line.Y2}] = true
}
points := make([][2]float64, 0, len(pointMap))
for p := range pointMap {
points = append(points, p)
}
return points
}
func castRays(cx, cy float64, lines []Line) []Line {
const rayLength = 2000.0
var rays []Line
points := getLinePoints(lines)
for _, p := range points {
l := Line{cx, cy, p[0], p[1]}
angle := l.angle()
for _, offset := range []float64{-0.001, 0.001} {
var intersections [][2]float64
ray := newRay(cx, cy, rayLength, angle+offset)
for _, wall := range lines {
if px, py, ok := intersection(ray, wall); ok {
intersections = append(intersections, [2]float64{px, py})
}
}
if len(intersections) > 0 {
minDist := math.Inf(1)
minIdx := -1
for i, point := range intersections {
dist := (cx-point[0])*(cx-point[0]) + (cy-point[1])*(cy-point[1])
if dist < minDist {
minDist = dist
minIdx = i
}
}
if minIdx >= 0 {
rays = append(rays, Line{cx, cy, intersections[minIdx][0], intersections[minIdx][1]})
}
}
}
}
sort.Slice(rays, func(i, j int) bool {
return rays[i].angle() < rays[j].angle()
})
return rays
}
// rayVertices creates vertices for a triangle between rays
func rayVertices(x1, y1, x2, y2, x3, y3 float64) []ebiten.Vertex {
return []ebiten.Vertex{
{DstX: float32(x1), DstY: float32(y1), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
{DstX: float32(x2), DstY: float32(y2), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
{DstX: float32(x3), DstY: float32(y3), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
}
}
func (s *System) Draw(screen *ebiten.Image, w *world.World) {
if !s.enabled || len(s.lights) == 0 {
return
}
lines := worldToLines(w)
sw := float64(s.screenWidth)
sh := float64(s.screenHeight)
lines = append(lines,
Line{0, 0, sw, 0},
Line{sw, 0, sw, sh},
Line{sw, sh, 0, sh},
Line{0, sh, 0, 0},
)
s.shadowImage.Fill(color.RGBA{0, 0, 0, 180})
for _, light := range s.lights {
rays := castRays(light.X, light.Y, lines)
opt := &ebiten.DrawTrianglesOptions{}
opt.Blend = ebiten.BlendSourceOut
for i, ray := range rays {
nextRay := rays[(i+1)%len(rays)]
v := rayVertices(light.X, light.Y, nextRay.X2, nextRay.Y2, ray.X2, ray.Y2)
s.shadowImage.DrawTriangles(v, []uint16{0, 1, 2}, s.triangleImage, opt)
}
}
shadowOpt := &ebiten.DrawImageOptions{}
shadowOpt.ColorScale.ScaleAlpha(float32(s.shadowIntensity))
screen.DrawImage(s.shadowImage, shadowOpt)
}
func (s *System) DrawDebug(screen *ebiten.Image, w *world.World) {
if !s.enabled || len(s.lights) == 0 {
return
}
lines := worldToLines(w)
sw := float64(s.screenWidth)
sh := float64(s.screenHeight)
lines = append(lines,
Line{0, 0, sw, 0},
Line{sw, 0, sw, sh},
Line{sw, sh, 0, sh},
Line{0, sh, 0, 0},
)
for _, light := range s.lights {
rays := castRays(light.X, light.Y, lines)
for _, ray := range rays {
drawLine(screen, ray, color.RGBA{255, 255, 0, 150})
}
drawCircle(screen, light.X, light.Y, 5, color.RGBA{255, 200, 100, 255})
}
}
// Helper function to draw a line
func drawLine(screen *ebiten.Image, l Line, c color.RGBA) {
x1, y1 := int(l.X1), int(l.Y1)
x2, y2 := int(l.X2), int(l.Y2)
dx := abs(x2 - x1)
dy := abs(y2 - y1)
sx := -1
if x1 < x2 {
sx = 1
}
sy := -1
if y1 < y2 {
sy = 1
}
err := dx - dy
for {
if x1 >= 0 && x1 < screen.Bounds().Dx() && y1 >= 0 && y1 < screen.Bounds().Dy() {
screen.Set(x1, y1, c)
}
if x1 == x2 && y1 == y2 {
break
}
e2 := 2 * err
if e2 > -dy {
err -= dy
x1 += sx
}
if e2 < dx {
err += dx
y1 += sy
}
}
}
// Helper function to draw a circle
func drawCircle(screen *ebiten.Image, cx, cy, radius float64, c color.RGBA) {
for angle := 0.0; angle < 2*math.Pi; angle += 0.1 {
x := int(cx + radius*math.Cos(angle))
y := int(cy + radius*math.Sin(angle))
if x >= 0 && x < screen.Bounds().Dx() && y >= 0 && y < screen.Bounds().Dy() {
screen.Set(x, y, c)
}
}
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func (s *System) IsPointInShadow(x, y float64, w *world.World) bool {
if !s.enabled || len(s.lights) == 0 {
return false
}
lines := worldToLines(w)
for _, light := range s.lights {
rayToPoint := Line{light.X, light.Y, x, y}
distanceToPoint := math.Sqrt((x-light.X)*(x-light.X) + (y-light.Y)*(y-light.Y))
blocked := false
for _, wall := range lines {
if ix, iy, ok := intersection(rayToPoint, wall); ok {
distanceToIntersection := math.Sqrt((ix-light.X)*(ix-light.X) + (iy-light.Y)*(iy-light.Y))
if distanceToIntersection < distanceToPoint-5.0 {
blocked = true
break
}
}
}
if !blocked {
return false
}
}
return true
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/atridad/LilGuy/internal/maps" "github.com/atridad/LilGuy/internal/maps"
"github.com/atridad/LilGuy/internal/portal" "github.com/atridad/LilGuy/internal/portal"
"github.com/atridad/LilGuy/internal/projectile" "github.com/atridad/LilGuy/internal/projectile"
"github.com/atridad/LilGuy/internal/raycast"
"github.com/atridad/LilGuy/internal/save" "github.com/atridad/LilGuy/internal/save"
"github.com/atridad/LilGuy/internal/status" "github.com/atridad/LilGuy/internal/status"
"github.com/atridad/LilGuy/internal/ui/hud" "github.com/atridad/LilGuy/internal/ui/hud"
@@ -48,9 +49,14 @@ type GameplayScreen struct {
showSaveNotification bool showSaveNotification bool
portalVisibility *bool portalVisibility *bool
// Raycasting system
raycastSystem *raycast.System
raycastEnabled *bool
raycastDebugMode *bool
} }
func NewGameplayScreen(screenWidth, screenHeight int, mapManager *maps.Manager, fpsEnabled *bool, portalVisibility *bool) *GameplayScreen { func NewGameplayScreen(screenWidth, screenHeight int, mapManager *maps.Manager, fpsEnabled *bool, portalVisibility *bool, raycastEnabled *bool, raycastDebugMode *bool) *GameplayScreen {
cfg := config.Default() cfg := config.Default()
// Ensure we have a current map // Ensure we have a current map
@@ -100,6 +106,9 @@ func NewGameplayScreen(screenWidth, screenHeight int, mapManager *maps.Manager,
gameStartTime: time.Now(), gameStartTime: time.Now(),
fpsEnabled: fpsEnabled, fpsEnabled: fpsEnabled,
portalVisibility: portalVisibility, portalVisibility: portalVisibility,
raycastSystem: raycast.NewSystem(screenWidth, screenHeight),
raycastEnabled: raycastEnabled,
raycastDebugMode: raycastDebugMode,
} }
gs.portals.OnTransition = gs.handlePortalTransition gs.portals.OnTransition = gs.handlePortalTransition
@@ -132,6 +141,9 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
g.projectiles.Update(dt, g.bounds.Width, g.bounds.Height) g.projectiles.Update(dt, g.bounds.Width, g.bounds.Height)
g.portals.Update(dt) g.portals.Update(dt)
// Update raycasting lights based on current map
g.updateRaycastLights()
// check for portal collisions // check for portal collisions
g.checkPortalCollision() g.checkPortalCollision()
@@ -148,6 +160,40 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
} }
} }
func (g *GameplayScreen) updateRaycastLights() {
g.raycastSystem.ClearLights()
currentMap := g.mapManager.CurrentMap()
if currentMap == nil {
return
}
screenWidth := g.bounds.Width
lightX := float64(screenWidth - 80)
lightY := 80.0
if currentMap.TimeOfDay == maps.Daytime {
g.raycastSystem.SetShadowIntensity(0.25)
g.raycastSystem.AddLight(&raycast.LightSource{
X: lightX,
Y: lightY,
Radius: 800.0,
Color: color.RGBA{R: 255, G: 250, B: 220, A: 255},
Intensity: 1.0,
})
} else {
g.raycastSystem.SetShadowIntensity(0.7)
g.raycastSystem.AddLight(&raycast.LightSource{
X: lightX,
Y: lightY,
Radius: 500.0,
Color: color.RGBA{R: 200, G: 220, B: 255, A: 255},
Intensity: 0.7,
})
}
}
// portal collision detection // portal collision detection
func (g *GameplayScreen) checkPortalCollision() { func (g *GameplayScreen) checkPortalCollision() {
heroRadius := g.hero.Radius heroRadius := g.hero.Radius
@@ -229,6 +275,26 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
if currentMap != nil { if currentMap != nil {
currentMap.Draw(screen) currentMap.Draw(screen)
g.hud.ScreenName = currentMap.DisplayName g.hud.ScreenName = currentMap.DisplayName
// Apply nighttime darkening overlay BEFORE raycasting
if currentMap.TimeOfDay == maps.Nighttime {
g.drawNightOverlay(screen)
}
// Draw raycasting shadows/lighting
if g.raycastEnabled != nil && *g.raycastEnabled {
g.raycastSystem.Draw(screen, g.world)
if g.raycastDebugMode != nil && *g.raycastDebugMode {
g.raycastSystem.DrawDebug(screen, g.world)
}
}
// Draw sun or moon circle in top-right
g.drawCelestialBody(screen, currentMap)
// Update hero lighting state
g.updateHeroLighting(currentMap)
} else { } else {
screen.Fill(cfg.Visual.BackgroundColor) screen.Fill(cfg.Visual.BackgroundColor)
} }
@@ -307,6 +373,55 @@ func (g *GameplayScreen) drawSaveNotification(screen *ebiten.Image) {
ebitenutil.DebugPrintAt(screen, msg, int(textX), int(textY)) ebitenutil.DebugPrintAt(screen, msg, int(textX), int(textY))
} }
func (g *GameplayScreen) updateHeroLighting(currentMap *maps.Map) {
if g.raycastEnabled == nil || !*g.raycastEnabled {
g.hero.InShadow = false
g.hero.ShadowIntensity = 0
return
}
// Check if hero is in shadow
inShadow := g.raycastSystem.IsPointInShadow(g.hero.X, g.hero.Y, g.world)
g.hero.InShadow = inShadow
if inShadow {
if currentMap.TimeOfDay == maps.Daytime {
g.hero.ShadowIntensity = 0.3
} else {
g.hero.ShadowIntensity = 0.6
}
} else {
g.hero.ShadowIntensity = 0
}
}
func (g *GameplayScreen) drawNightOverlay(screen *ebiten.Image) {
opts := &ebiten.DrawImageOptions{}
opts.ColorScale.Scale(0.4, 0.4, 0.6, 1.0)
overlay := ebiten.NewImage(int(g.bounds.Width), int(g.bounds.Height))
overlay.Fill(color.RGBA{R: 0, G: 0, B: 30, A: 120})
screen.DrawImage(overlay, &ebiten.DrawImageOptions{})
}
func (g *GameplayScreen) drawCelestialBody(screen *ebiten.Image, currentMap *maps.Map) {
if currentMap == nil {
return
}
cx := float32(g.bounds.Width - 80)
cy := float32(80)
radius := float32(25)
if currentMap.TimeOfDay == maps.Daytime {
vector.DrawFilledCircle(screen, cx, cy, radius, color.RGBA{R: 255, G: 230, B: 100, A: 255}, true)
vector.DrawFilledCircle(screen, cx, cy, radius+3, color.RGBA{R: 255, G: 240, B: 150, A: 100}, true)
} else {
vector.DrawFilledCircle(screen, cx, cy, radius, color.RGBA{R: 240, G: 240, B: 255, A: 255}, true)
vector.DrawFilledCircle(screen, cx, cy, radius+3, color.RGBA{R: 200, G: 200, B: 255, A: 80}, true)
}
}
func (g *GameplayScreen) ShowSaveNotification() { func (g *GameplayScreen) ShowSaveNotification() {
g.showSaveNotification = true g.showSaveNotification = true
g.saveNotificationTimer = config.SaveNotificationDuration g.saveNotificationTimer = config.SaveNotificationDuration
@@ -431,3 +546,33 @@ func (g *GameplayScreen) LoadState(state *save.GameState) {
g.fpsAccumulator = 0 g.fpsAccumulator = 0
g.fpsValue = 0 g.fpsValue = 0
} }
// ToggleRaycast toggles the raycasting lighting system on/off
func (g *GameplayScreen) ToggleRaycast() {
if g.raycastEnabled != nil {
*g.raycastEnabled = !*g.raycastEnabled
}
}
// ToggleRaycastDebug toggles the raycasting debug visualization
func (g *GameplayScreen) ToggleRaycastDebug() {
if g.raycastDebugMode != nil {
*g.raycastDebugMode = !*g.raycastDebugMode
}
}
// IsRaycastEnabled returns whether raycasting is enabled
func (g *GameplayScreen) IsRaycastEnabled() bool {
if g.raycastEnabled == nil {
return false
}
return *g.raycastEnabled
}
// IsRaycastDebugMode returns whether raycasting debug mode is enabled
func (g *GameplayScreen) IsRaycastDebugMode() bool {
if g.raycastDebugMode == nil {
return false
}
return *g.raycastDebugMode
}

View File

@@ -27,6 +27,8 @@ type SettingsScreen struct {
fpsMonitorValue *bool fpsMonitorValue *bool
fpsCapValue FPSCapSetting fpsCapValue FPSCapSetting
portalVisibilityValue *bool portalVisibilityValue *bool
raycastEnabledValue *bool
raycastDebugValue *bool
} }
func NewSettingsScreen() *SettingsScreen { func NewSettingsScreen() *SettingsScreen {
@@ -48,6 +50,11 @@ func (s *SettingsScreen) SetPortalVisibility(enabled *bool) {
s.portalVisibilityValue = enabled s.portalVisibilityValue = enabled
} }
func (s *SettingsScreen) SetRaycastSettings(enabled *bool, debugMode *bool) {
s.raycastEnabledValue = enabled
s.raycastDebugValue = debugMode
}
func (s *SettingsScreen) Update() bool { func (s *SettingsScreen) Update() bool {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
if s.currentScreen == settingsDebugOptions { if s.currentScreen == settingsDebugOptions {
@@ -92,7 +99,7 @@ func (s *SettingsScreen) updateMain() bool {
} }
func (s *SettingsScreen) updateDebugOptions() bool { func (s *SettingsScreen) updateDebugOptions() bool {
debugOptionsCount := 3 debugOptionsCount := 5 // FPS Monitor, Portal Visibility, Raycast Enabled, Raycast Debug, Back
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
s.selectedIndex-- s.selectedIndex--
if s.selectedIndex < 0 { if s.selectedIndex < 0 {
@@ -111,7 +118,11 @@ func (s *SettingsScreen) updateDebugOptions() bool {
*s.fpsMonitorValue = !*s.fpsMonitorValue *s.fpsMonitorValue = !*s.fpsMonitorValue
} else if s.selectedIndex == 1 && s.portalVisibilityValue != nil { } else if s.selectedIndex == 1 && s.portalVisibilityValue != nil {
*s.portalVisibilityValue = !*s.portalVisibilityValue *s.portalVisibilityValue = !*s.portalVisibilityValue
} else if s.selectedIndex == 2 { } else if s.selectedIndex == 2 && s.raycastEnabledValue != nil {
*s.raycastEnabledValue = !*s.raycastEnabledValue
} else if s.selectedIndex == 3 && s.raycastDebugValue != nil {
*s.raycastDebugValue = !*s.raycastDebugValue
} else if s.selectedIndex == 4 {
s.currentScreen = settingsMain s.currentScreen = settingsMain
s.selectedIndex = 0 s.selectedIndex = 0
} }
@@ -211,10 +222,44 @@ func (s *SettingsScreen) drawDebugOptions(screen *ebiten.Image, screenWidth, scr
s.drawText(screen, portalVisText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, portalY) s.drawText(screen, portalVisText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, portalY)
} }
// Raycast enabled toggle
raycastText := "Lighting: "
if s.raycastEnabledValue != nil && *s.raycastEnabledValue {
raycastText += "ON"
} else {
raycastText += "OFF"
}
raycastY := startY + 80
if s.selectedIndex == 2 {
indicatorX := leftMargin - 20
s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, raycastY)
s.drawText(screen, raycastText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, raycastY)
} else {
s.drawText(screen, raycastText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, raycastY)
}
// Raycast debug toggle
raycastDebugText := "Debug Rays: "
if s.raycastDebugValue != nil && *s.raycastDebugValue {
raycastDebugText += "ON"
} else {
raycastDebugText += "OFF"
}
raycastDebugY := startY + 120
if s.selectedIndex == 3 {
indicatorX := leftMargin - 20
s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, raycastDebugY)
s.drawText(screen, raycastDebugText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, raycastDebugY)
} else {
s.drawText(screen, raycastDebugText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, raycastDebugY)
}
// back option // back option
backText := "< Back" backText := "< Back"
backY := startY + 80 backY := startY + 160
if s.selectedIndex == 2 { if s.selectedIndex == 4 {
indicatorX := leftMargin - 20 indicatorX := leftMargin - 20
s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, backY) s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, backY)
s.drawText(screen, backText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, backY) s.drawText(screen, backText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, backY)

View File

@@ -55,6 +55,10 @@ func (t *TitleScreen) SetPortalVisibility(enabled *bool) {
t.settingsScreen.SetPortalVisibility(enabled) t.settingsScreen.SetPortalVisibility(enabled)
} }
func (t *TitleScreen) SetRaycastSettings(enabled *bool, debugMode *bool) {
t.settingsScreen.SetRaycastSettings(enabled, debugMode)
}
func (t *TitleScreen) SetHasSaveGame(hasSave bool) { func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
t.hasSaveGame = hasSave t.hasSaveGame = hasSave
if !hasSave && t.selectedIndex == 0 { if !hasSave && t.selectedIndex == 0 {

View File

@@ -88,6 +88,10 @@ func (m *PauseMenu) SetPortalVisibility(enabled *bool) {
m.settingsScreen.SetPortalVisibility(enabled) m.settingsScreen.SetPortalVisibility(enabled)
} }
func (m *PauseMenu) SetRaycastSettings(enabled *bool, debugMode *bool) {
m.settingsScreen.SetRaycastSettings(enabled, debugMode)
}
// Update logic // Update logic
func (m *PauseMenu) Update() *MenuOption { func (m *PauseMenu) Update() *MenuOption {