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 } if dt > 0.1 { dt = 0.1 } 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 } drawX := float32(int(p.X + 0.5)) drawY := float32(int(p.Y + 0.5)) drawWidth := float32(int(p.Width + 0.5)) drawHeight := float32(int(p.Height + 0.5)) vector.FillRect( screen, drawX, drawY, drawWidth, drawHeight, p.Color, false, ) if p.Enabled { glowPulse := 0.5 + 0.5*float64(p.GlowIntensity)/6.28 glowAlpha := uint8(float64(p.GlowColor.A) * glowPulse) glowColor := color.NRGBA{ R: p.GlowColor.R, G: p.GlowColor.G, B: p.GlowColor.B, A: glowAlpha, } borderWidth := float32(4) vector.StrokeRect( screen, drawX-borderWidth/2, drawY-borderWidth/2, drawWidth+borderWidth, drawHeight+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) { // clamp delta time to prevent physics issues if dt > 0.1 { dt = 0.1 } for _, p := range m.Portals { p.Update(dt) } if m.transitionCooldown > 0 { m.transitionCooldown -= dt if m.transitionCooldown < 0 { m.transitionCooldown = 0 } } } 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) }