Lighting experiments
This commit is contained in:
16
go.mod
16
go.mod
@@ -3,19 +3,19 @@ module github.com/atridad/LilGuy
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/hajimehoshi/ebiten/v2 v2.9.4
|
||||
golang.org/x/image v0.31.0
|
||||
github.com/hajimehoshi/ebiten/v2 v2.9.6
|
||||
golang.org/x/image v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
|
||||
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/go-text/typesetting v0.3.0 // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/go-text/typesetting v0.3.1 // indirect
|
||||
github.com/jezek/xgb v1.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
||||
17
go.sum
17
go.sum
@@ -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/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.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/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/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/go.mod h1:/PD+aLjAJ0F2UoQx6hkOfXqWN7BkroDUMr5W+IT1dpE=
|
||||
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.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/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/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
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/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/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/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/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=
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/maps"
|
||||
"github.com/atridad/LilGuy/internal/save"
|
||||
"github.com/atridad/LilGuy/internal/screens"
|
||||
"github.com/atridad/LilGuy/internal/ui/menu"
|
||||
"github.com/atridad/LilGuy/internal/maps"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -105,6 +105,8 @@ type state struct {
|
||||
fpsEnabled bool
|
||||
fpsCap FPSCap
|
||||
portalVisibility bool
|
||||
raycastEnabled bool
|
||||
raycastDebugMode bool
|
||||
saveManager *save.Manager
|
||||
|
||||
lastAutoSave time.Time
|
||||
@@ -123,6 +125,8 @@ func newState() *state {
|
||||
fpsEnabled: false,
|
||||
fpsCap: FPSCap60,
|
||||
portalVisibility: false,
|
||||
raycastEnabled: true,
|
||||
raycastDebugMode: false,
|
||||
lastAutoSave: now,
|
||||
autoSaveInterval: 30 * time.Second,
|
||||
}
|
||||
@@ -165,16 +169,18 @@ func newState() *state {
|
||||
// Initialize screens
|
||||
s.splashScreen = screens.NewSplashScreen()
|
||||
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()
|
||||
|
||||
// Wire up settings references
|
||||
s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
|
||||
s.titleScreen.SetFPSCap(&s.fpsCap)
|
||||
s.titleScreen.SetPortalVisibility(&s.portalVisibility)
|
||||
s.titleScreen.SetRaycastSettings(&s.raycastEnabled, &s.raycastDebugMode)
|
||||
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
|
||||
s.pauseMenu.SetFPSCap(&s.fpsCap)
|
||||
s.pauseMenu.SetPortalVisibility(&s.portalVisibility)
|
||||
s.pauseMenu.SetRaycastSettings(&s.raycastEnabled, &s.raycastDebugMode)
|
||||
|
||||
if saveManager != nil {
|
||||
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
|
||||
@@ -282,6 +288,16 @@ func (g *Game) updatePlaying() error {
|
||||
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()
|
||||
delta := now.Sub(g.state.lastTick)
|
||||
g.state.lastTick = now
|
||||
|
||||
@@ -90,6 +90,10 @@ type Hero struct {
|
||||
lastAnimKey animationKey
|
||||
|
||||
ProjectileConfig projectile.ProjectileConfig
|
||||
|
||||
// Lighting state
|
||||
InShadow bool
|
||||
ShadowIntensity float64
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -358,6 +362,11 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
// 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
|
||||
screen.DrawImage(sprite, op)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
func CreateDesert(screenWidth, screenHeight float64) *Map {
|
||||
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
|
||||
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},
|
||||
})
|
||||
|
||||
// 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
|
||||
leftPortal := portal.CreateSidePortal("desert_left", portal.SideLeft, screenWidth, screenHeight)
|
||||
leftPortal.DestinationMap = "plains"
|
||||
|
||||
@@ -3,9 +3,16 @@ package maps
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/atridad/LilGuy/internal/portal"
|
||||
"github.com/atridad/LilGuy/internal/world"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type TimeOfDay string
|
||||
|
||||
const (
|
||||
Daytime TimeOfDay = "day"
|
||||
Nighttime TimeOfDay = "night"
|
||||
)
|
||||
|
||||
type Map struct {
|
||||
@@ -17,6 +24,7 @@ type Map struct {
|
||||
World *world.World
|
||||
Portals []*portal.Portal
|
||||
BackgroundColor color.NRGBA
|
||||
TimeOfDay TimeOfDay // Day or Night tag
|
||||
|
||||
bakedImage *ebiten.Image
|
||||
}
|
||||
@@ -31,6 +39,7 @@ func NewMap(id string, number int, displayName string, width, height float64) *M
|
||||
World: world.NewWorld(),
|
||||
Portals: make([]*portal.Portal, 0),
|
||||
BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255},
|
||||
TimeOfDay: Daytime, // Default to daytime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
func CreatePlains(screenWidth, screenHeight float64) *Map {
|
||||
m := NewMap("plains", 1, "Plains", screenWidth, screenHeight)
|
||||
m.BackgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
|
||||
m.TimeOfDay = Daytime
|
||||
|
||||
// ground surface
|
||||
groundColor := color.NRGBA{R: 34, G: 139, B: 34, A: 255}
|
||||
|
||||
353
internal/raycast/raycast.go
Normal file
353
internal/raycast/raycast.go
Normal 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
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/atridad/LilGuy/internal/maps"
|
||||
"github.com/atridad/LilGuy/internal/portal"
|
||||
"github.com/atridad/LilGuy/internal/projectile"
|
||||
"github.com/atridad/LilGuy/internal/raycast"
|
||||
"github.com/atridad/LilGuy/internal/save"
|
||||
"github.com/atridad/LilGuy/internal/status"
|
||||
"github.com/atridad/LilGuy/internal/ui/hud"
|
||||
@@ -48,9 +49,14 @@ type GameplayScreen struct {
|
||||
showSaveNotification 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()
|
||||
|
||||
// Ensure we have a current map
|
||||
@@ -100,6 +106,9 @@ func NewGameplayScreen(screenWidth, screenHeight int, mapManager *maps.Manager,
|
||||
gameStartTime: time.Now(),
|
||||
fpsEnabled: fpsEnabled,
|
||||
portalVisibility: portalVisibility,
|
||||
raycastSystem: raycast.NewSystem(screenWidth, screenHeight),
|
||||
raycastEnabled: raycastEnabled,
|
||||
raycastDebugMode: raycastDebugMode,
|
||||
}
|
||||
|
||||
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.portals.Update(dt)
|
||||
|
||||
// Update raycasting lights based on current map
|
||||
g.updateRaycastLights()
|
||||
|
||||
// check for portal collisions
|
||||
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
|
||||
func (g *GameplayScreen) checkPortalCollision() {
|
||||
heroRadius := g.hero.Radius
|
||||
@@ -229,6 +275,26 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||
if currentMap != nil {
|
||||
currentMap.Draw(screen)
|
||||
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 {
|
||||
screen.Fill(cfg.Visual.BackgroundColor)
|
||||
}
|
||||
@@ -307,6 +373,55 @@ func (g *GameplayScreen) drawSaveNotification(screen *ebiten.Image) {
|
||||
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() {
|
||||
g.showSaveNotification = true
|
||||
g.saveNotificationTimer = config.SaveNotificationDuration
|
||||
@@ -431,3 +546,33 @@ func (g *GameplayScreen) LoadState(state *save.GameState) {
|
||||
g.fpsAccumulator = 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
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ type SettingsScreen struct {
|
||||
fpsMonitorValue *bool
|
||||
fpsCapValue FPSCapSetting
|
||||
portalVisibilityValue *bool
|
||||
raycastEnabledValue *bool
|
||||
raycastDebugValue *bool
|
||||
}
|
||||
|
||||
func NewSettingsScreen() *SettingsScreen {
|
||||
@@ -48,6 +50,11 @@ func (s *SettingsScreen) SetPortalVisibility(enabled *bool) {
|
||||
s.portalVisibilityValue = enabled
|
||||
}
|
||||
|
||||
func (s *SettingsScreen) SetRaycastSettings(enabled *bool, debugMode *bool) {
|
||||
s.raycastEnabledValue = enabled
|
||||
s.raycastDebugValue = debugMode
|
||||
}
|
||||
|
||||
func (s *SettingsScreen) Update() bool {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
if s.currentScreen == settingsDebugOptions {
|
||||
@@ -92,7 +99,7 @@ func (s *SettingsScreen) updateMain() 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) {
|
||||
s.selectedIndex--
|
||||
if s.selectedIndex < 0 {
|
||||
@@ -111,7 +118,11 @@ func (s *SettingsScreen) updateDebugOptions() bool {
|
||||
*s.fpsMonitorValue = !*s.fpsMonitorValue
|
||||
} else if s.selectedIndex == 1 && s.portalVisibilityValue != nil {
|
||||
*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.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)
|
||||
}
|
||||
|
||||
// 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
|
||||
backText := "< Back"
|
||||
backY := startY + 80
|
||||
if s.selectedIndex == 2 {
|
||||
backY := startY + 160
|
||||
if s.selectedIndex == 4 {
|
||||
indicatorX := leftMargin - 20
|
||||
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)
|
||||
|
||||
@@ -55,6 +55,10 @@ func (t *TitleScreen) SetPortalVisibility(enabled *bool) {
|
||||
t.settingsScreen.SetPortalVisibility(enabled)
|
||||
}
|
||||
|
||||
func (t *TitleScreen) SetRaycastSettings(enabled *bool, debugMode *bool) {
|
||||
t.settingsScreen.SetRaycastSettings(enabled, debugMode)
|
||||
}
|
||||
|
||||
func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
|
||||
t.hasSaveGame = hasSave
|
||||
if !hasSave && t.selectedIndex == 0 {
|
||||
|
||||
@@ -88,6 +88,10 @@ func (m *PauseMenu) SetPortalVisibility(enabled *bool) {
|
||||
m.settingsScreen.SetPortalVisibility(enabled)
|
||||
}
|
||||
|
||||
func (m *PauseMenu) SetRaycastSettings(enabled *bool, debugMode *bool) {
|
||||
m.settingsScreen.SetRaycastSettings(enabled, debugMode)
|
||||
}
|
||||
|
||||
// Update logic
|
||||
|
||||
func (m *PauseMenu) Update() *MenuOption {
|
||||
|
||||
Reference in New Issue
Block a user