package scenes import ( "fmt" "image" "image/color" _ "image/jpeg" _ "image/png" "log" "sort" "mountain/internal/entities" "mountain/internal/input" "github.com/go-gl/mathgl/mgl64" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" ) const ( Gravity = 0.5 JumpForce = -8.0 MoveSpeed = 4.0 ViewSize = 380 // 800 width, we have 2 viewports (380 each with 20 padding) ) type PlayState int const ( StatePlaying PlayState = iota StatePaused StateSettings ) type Player struct { Pos mgl64.Vec3 // 3D Kordinater: X (vänster/höger), Y (upp/ner), Z (djup) Vel mgl64.Vec3 // Hastighet Width, Height float64 // För (AABB) IsGrounded bool // Sprites SpriteRun *ebiten.Image SpriteJump *ebiten.Image SpriteIdle *ebiten.Image SpriteTop *ebiten.Image AnimationFrame int AnimationCounter int FacingRight bool } type PlayScene struct { world *entities.World player *Player leftView *ebiten.Image rightView *ebiten.Image state PlayState // Settings UI state sortedActions []string selectedIndex int } func NewPlayScene() *PlayScene { w, err := entities.LoadWorldFromTiled("assets/maps/room1.tmj") if err != nil { log.Println("Kunde inte ladda room1:", err) w = entities.NewWorld(100, 10, 100) // Fallback tom värd om JSON saknas } pRun, _, err := ebitenutil.NewImageFromFile("assets/images/Warrior_1/Run.png") if err != nil { log.Println("Run sprite error:", err) pRun = ebiten.NewImage(96, 96) } pIdle, _, _ := ebitenutil.NewImageFromFile("assets/images/Warrior_1/Idle.png") if pIdle == nil { pIdle = pRun } pJump, _, _ := ebitenutil.NewImageFromFile("assets/images/Warrior_1/Jump.png") if pJump == nil { pJump = pRun } pTop := ebiten.NewImage(96, 96) pTop.Fill(color.RGBA{255, 255, 0, 255}) player := &Player{ Pos: mgl64.Vec3{50.0 * entities.TileSize, 6.0 * entities.TileSize, 50.0 * entities.TileSize}, Width: 32, Height: 32, SpriteRun: pRun, SpriteIdle: pIdle, SpriteJump: pJump, SpriteTop: pTop, FacingRight: true, } return &PlayScene{ world: w, player: player, leftView: ebiten.NewImage(ViewSize, ViewSize), rightView: ebiten.NewImage(ViewSize, ViewSize), state: StatePlaying, sortedActions: make([]string, 0), selectedIndex: 0, } } func (s *PlayScene) Update() error { switch s.state { case StatePlaying: return s.updatePlaying() case StatePaused: return s.updatePaused() case StateSettings: return s.updateSettings() } return nil } func (s *PlayScene) updatePaused() error { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { s.state = StatePlaying } // Menu logic for pausing if inpututil.IsKeyJustPressed(ebiten.KeyEnter) { if s.selectedIndex == 0 { s.state = StatePlaying } else if s.selectedIndex == 1 { s.state = StateSettings s.initSettings() } } if inpututil.IsKeyJustPressed(ebiten.KeyUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { s.selectedIndex-- if s.selectedIndex < 0 { s.selectedIndex = 1 } } if inpututil.IsKeyJustPressed(ebiten.KeyDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) { s.selectedIndex++ if s.selectedIndex > 1 { s.selectedIndex = 0 } } return nil } func (s *PlayScene) initSettings() { s.sortedActions = make([]string, 0, len(input.Manager.Actions)) for k := range input.Manager.Actions { s.sortedActions = append(s.sortedActions, k) } sort.Strings(s.sortedActions) s.selectedIndex = 0 } func (s *PlayScene) updateSettings() error { if input.Manager.IsRecording() { if input.Manager.UpdateRecording() { // Recording finished } return nil } if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { s.state = StatePaused s.selectedIndex = 1 // back to settings button return nil } if inpututil.IsKeyJustPressed(ebiten.KeyUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { s.selectedIndex-- if s.selectedIndex < 0 { s.selectedIndex = len(s.sortedActions) - 1 } } if inpututil.IsKeyJustPressed(ebiten.KeyDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) { s.selectedIndex++ if s.selectedIndex >= len(s.sortedActions) { s.selectedIndex = 0 } } if inpututil.IsKeyJustPressed(ebiten.KeyEnter) { action := s.sortedActions[s.selectedIndex] input.Manager.StartRecording(action) } return nil } func (s *PlayScene) updatePlaying() error { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { s.state = StatePaused s.selectedIndex = 0 return nil } p := s.player currentMoveSpeed := MoveSpeed if input.Manager.IsActionPressed("Run") { currentMoveSpeed = MoveSpeed * 1.8 // Lite snabbare när man springer } p.Vel[0] = 0 p.Vel[2] = 0 isMoving := false // Kolla om spelaren är i ett klättringsbart objekt currentGridX := int((p.Pos[0] + p.Width/2) / entities.TileSize) currentGridY := int((p.Pos[1] + p.Height/2) / entities.TileSize) currentGridZ := int((p.Pos[2] + p.Width/2) / entities.TileSize) entIn := s.world.GetEntityAt(currentGridX, currentGridY, currentGridZ) isClimbing := false if entIn != nil && entIn.IsClimbable() && input.Manager.IsActionPressed("ClimbMode") { isClimbing = true } if isClimbing { p.Vel[1] = 0 // Ingen gravitation när man klättrar if input.Manager.IsActionPressed("ClimbLeft") { p.Vel[0] = -currentMoveSpeed p.FacingRight = false isMoving = true } if input.Manager.IsActionPressed("ClimbRight") { p.Vel[0] = currentMoveSpeed p.FacingRight = true isMoving = true } if input.Manager.IsActionPressed("ClimbUp") { p.Vel[1] = -currentMoveSpeed // Uppåt isMoving = true } if input.Manager.IsActionPressed("ClimbDown") { p.Vel[1] = currentMoveSpeed // Nedåt isMoving = true } if input.Manager.IsActionPressed("ClimbIn") { p.Vel[2] = -currentMoveSpeed // Inåt (mot djupet Z-) isMoving = true } if input.Manager.IsActionPressed("ClimbOut") { p.Vel[2] = currentMoveSpeed // Utåt (från djupet Z+) isMoving = true } } else { if input.Manager.IsActionPressed("MoveLeft") { p.Vel[0] = -currentMoveSpeed p.FacingRight = false isMoving = true } if input.Manager.IsActionPressed("MoveRight") { p.Vel[0] = currentMoveSpeed p.FacingRight = true isMoving = true } if input.Manager.IsActionPressed("MoveUp") { p.Vel[2] = -currentMoveSpeed isMoving = true } if input.Manager.IsActionPressed("MoveDown") { p.Vel[2] = currentMoveSpeed isMoving = true } if input.Manager.IsActionPressed("Jump") && p.IsGrounded { p.Vel[1] = JumpForce p.IsGrounded = false } if !p.IsGrounded { p.Vel[1] += Gravity } } // === FYSIK OCH KOLLISION === // Få tag på det nuvarande objektet i spelarens mitt-punkt för att veta om vi står i något klättringsbart currGridX := int((p.Pos[0] + p.Width/2) / entities.TileSize) currGridY := int((p.Pos[1] + p.Height/2) / entities.TileSize) currGridZ := int((p.Pos[2] + p.Width/2) / entities.TileSize) currentEnt := s.world.GetEntityAt(currGridX, currGridY, currGridZ) isCurrentClimbable := currentEnt != nil && currentEnt.IsClimbable() // 1. Hantera kollision för X-axeln (vänster/höger) nextX := p.Pos[0] + p.Vel[0] if p.Vel[0] != 0 { targetGridX := int((nextX + p.Width/2) / entities.TileSize) entX := s.world.GetEntityAt(targetGridX, currGridY, currGridZ) if entX != nil && entX.IsBlocking() { isTargetClimbable := entX.IsClimbable() // Auto-klättra om målet är klättringsbart ELLER om vi redan står i något klättringsbart (t.ex framför en vägg på en stege) if isTargetClimbable || isCurrentClimbable { nextX = p.Pos[0] // Avbryt rörelse framåt p.Vel[0] = 0 p.Pos[1] -= MoveSpeed * 0.6 // Långsam rörelse uppåt ("klättra upp") p.IsGrounded = false } else { // Vanlig vägg - stoppa rörelse helt nextX = p.Pos[0] p.Vel[0] = 0 } } } p.Pos[0] = nextX // 2. Hantera kollision för Z-axeln (djup in/ut) nextZ := p.Pos[2] + p.Vel[2] if p.Vel[2] != 0 { // Uppdatera currGridX eftersom vi kan ha rört oss currGridX = int((p.Pos[0] + p.Width/2) / entities.TileSize) targetGridZ := int((nextZ + p.Width/2) / entities.TileSize) entZ := s.world.GetEntityAt(currGridX, currGridY, targetGridZ) if entZ != nil && entZ.IsBlocking() { isTargetClimbable := entZ.IsClimbable() if isTargetClimbable || isCurrentClimbable { nextZ = p.Pos[2] // Avbryt rörelse framåt p.Vel[2] = 0 p.Pos[1] -= MoveSpeed * 0.6 // Långsam rörelse uppåt ("klättra upp") p.IsGrounded = false } else { // Vanlig vägg - stoppa rörelse helt nextZ = p.Pos[2] p.Vel[2] = 0 } } } p.Pos[2] = nextZ // 3. Hantera kollision för Y-axeln (upp/ner + gravitation) nextY := p.Pos[1] + p.Vel[1] gridX := int((p.Pos[0] + p.Width/2) / entities.TileSize) gridZ := int((p.Pos[2] + p.Width/2) / entities.TileSize) if p.Vel[1] >= 0 { // Faller (eller står still) - kolla kollision med marken (fötterna) gridYFeet := int((nextY + p.Height) / entities.TileSize) entY := s.world.GetEntityAt(gridX, gridYFeet, gridZ) if entY != nil && entY.IsBlocking() { nextY = float64(gridYFeet)*entities.TileSize - p.Height - 0.1 p.Vel[1] = 0 p.IsGrounded = true } else { p.IsGrounded = false } } else { // Hoppar eller klättrar uppåt - kolla kollision med taket (huvudet) p.IsGrounded = false gridYHead := int(nextY / entities.TileSize) entY := s.world.GetEntityAt(gridX, gridYHead, gridZ) // Vi studsar i taket om det är solid OCH inte är klättringsbart if entY != nil && entY.IsBlocking() && !entY.IsClimbable() { nextY = float64(gridYHead+1)*entities.TileSize + 0.1 p.Vel[1] = 0 } } p.Pos[1] = nextY // Kolla ifall vi står på en portal (mittpunkten) standEnt := s.world.GetEntityAt(gridX, int((p.Pos[1]+p.Height/2)/entities.TileSize), gridZ) if prt, ok := standEnt.(*entities.Portal); ok { log.Println("Byter rum till: ", prt.TargetMap) newW, err := entities.LoadWorldFromTiled("assets/maps/" + prt.TargetMap + ".tmj") if err == nil { s.world = newW // Reset player position for new map if prt.TargetMap == "room1" { p.Pos = mgl64.Vec3{80.0 * entities.TileSize, 6.0 * entities.TileSize, 10.0 * entities.TileSize} } else { p.Pos = mgl64.Vec3{40.0 * entities.TileSize, 6.0 * entities.TileSize, 65.0 * entities.TileSize} } } } if isMoving { p.AnimationCounter++ if p.AnimationCounter > 5 { p.AnimationFrame = (p.AnimationFrame + 1) % 6 p.AnimationCounter = 0 } } else { p.AnimationFrame = 0 } return nil } func (s *PlayScene) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{20, 20, 20, 255}) s.leftView.Fill(color.RGBA{135, 206, 235, 255}) s.rightView.Fill(color.RGBA{50, 150, 50, 255}) playerGridZ := int((s.player.Pos[2] + s.player.Width/2) / entities.TileSize) playerGridY := int((s.player.Pos[1] + s.player.Height/2) / entities.TileSize) camX := s.player.Pos[0] - float64(ViewSize)/2.0 camY := s.player.Pos[1] - float64(ViewSize)/2.0 camZ := s.player.Pos[2] - float64(ViewSize)/2.0 // Vänster vy: Sido-vy (renderar objekt vid spelarens Z, max -3 till +3 djup) leftDepthStart := playerGridZ - 3 if leftDepthStart < 0 { leftDepthStart = 0 } leftDepthEnd := playerGridZ + 3 if leftDepthEnd >= s.world.Depth { leftDepthEnd = s.world.Depth - 1 } for zLevel := leftDepthStart; zLevel <= leftDepthEnd; zLevel++ { alpha := float32(1.0) diff := float32(zLevel - playerGridZ) if diff < 0 { // Bakom spelaren: självande alpha alpha = 1.0 + (diff * 0.2) // diff är negativt, så detta minskar alpha if alpha < 0.2 { alpha = 0.2 } } else if diff > 0 { // Framför spelaren: självande alpha alpha = 1.0 - (diff * 0.3) if alpha < 0.1 { alpha = 0.1 } } // Rendera spelaren på rätt Z-lager (vid diff == 0) if zLevel == playerGridZ { for x := 0; x < s.world.Width; x++ { for y := 0; y < s.world.Height; y++ { ent := s.world.GetEntityAt(x, y, zLevel) if ent != nil { px := float64(x)*entities.TileSize - camX py := float64(y)*entities.TileSize - camY ent.DrawSide(s.leftView, px, py, 1.0) } } } s.drawPlayerSide(s.leftView, camX, camY) } else { for x := 0; x < s.world.Width; x++ { for y := 0; y < s.world.Height; y++ { ent := s.world.GetEntityAt(x, y, zLevel) if ent != nil { px := float64(x)*entities.TileSize - camX py := float64(y)*entities.TileSize - camY ent.DrawSide(s.leftView, px, py, alpha) } } } } } // Höger vy: Ovan-vy (renderar Y-lager runt spelaren) // Positivt 'diff' i Y-axel betyder djupare in i marken (Längre bort = mer transparent) // Negativt 'diff' i Y-axel betyder högre upp mot kameran (Närmare = mer transparent så man ser igenom) for diff := 3; diff >= -3; diff-- { yLevel := playerGridY + diff if yLevel >= 0 && yLevel < s.world.Height { alpha := float32(1.0) if diff > 0 { // Under marknivån: sjunkande alpha alpha = 1.0 - (float32(diff) * 0.2) if alpha < 0.2 { alpha = 0.2 } } else if diff < 0 { // Över marknivån: sjunkande alpha alpha = 1.0 - (float32(-diff) * 0.3) if alpha < 0.1 { alpha = 0.1 } } if diff == 0 { for x := 0; x < s.world.Width; x++ { for z := 0; z < s.world.Depth; z++ { ent := s.world.GetEntityAt(x, yLevel, z) if ent != nil { px := float64(x)*entities.TileSize - camX pz := float64(z)*entities.TileSize - camZ ent.DrawTop(s.rightView, px, pz, 1.0) } } } s.drawPlayerTop(s.rightView, camX, camZ) } else { for x := 0; x < s.world.Width; x++ { for z := 0; z < s.world.Depth; z++ { ent := s.world.GetEntityAt(x, yLevel, z) if ent != nil { px := float64(x)*entities.TileSize - camX pz := float64(z)*entities.TileSize - camZ ent.DrawTop(s.rightView, px, pz, alpha) } } } } } } opL := &ebiten.DrawImageOptions{} opL.GeoM.Translate(10, 100) screen.DrawImage(s.leftView, opL) opR := &ebiten.DrawImageOptions{} opR.GeoM.Translate(410, 100) screen.DrawImage(s.rightView, opR) ebitenutil.DebugPrint(screen, "VÄNSTER (Sido vy) HÖGER (Top vy: Slice av din Hojd)") // Rita UI ovanpå allt, beroende på state if s.state == StatePaused { s.drawPauseUI(screen) } else if s.state == StateSettings { s.drawSettingsUI(screen) } } func (s *PlayScene) drawPlayerSide(screen *ebiten.Image, camX, camY float64) { p := s.player sx := p.AnimationFrame * 96 srcRect := image.Rect(sx, 0, sx+96, 96) var activeSprite *ebiten.Image if p.Vel[0] != 0 || p.Vel[2] != 0 { // Is moving X or Z activeSprite = p.SpriteRun } else { activeSprite = p.SpriteIdle } subImg := activeSprite.SubImage(srcRect).(*ebiten.Image) op := &ebiten.DrawImageOptions{} if !p.FacingRight { op.GeoM.Scale(-1, 1) op.GeoM.Translate(96, 0) } drawX := p.Pos[0] - camX - 32 drawY := p.Pos[1] - camY - 64 op.GeoM.Translate(drawX, drawY) screen.DrawImage(subImg, op) } func (s *PlayScene) drawPlayerTop(screen *ebiten.Image, camX, camZ float64) { p := s.player sx := p.AnimationFrame * 96 srcRect := image.Rect(sx, 0, sx+96, 96) var activeSprite *ebiten.Image if p.Vel[0] != 0 || p.Vel[2] != 0 || p.Vel[1] != 0 { activeSprite = p.SpriteRun } else { activeSprite = p.SpriteIdle } subImg := activeSprite.SubImage(srcRect).(*ebiten.Image) op := &ebiten.DrawImageOptions{} if !p.FacingRight { op.GeoM.Scale(-1, 1) op.GeoM.Translate(96, 0) } drawX := p.Pos[0] - camX - 32 drawZ := p.Pos[2] - camZ - 64 op.GeoM.Translate(drawX, drawZ) // Rita med lite tint i toppen kanske? Vi ritar bara ut imagen screen.DrawImage(subImg, op) } func (s *PlayScene) drawPauseUI(screen *ebiten.Image) { // Mörka ner bakgrunden något overlay := ebiten.NewImage(800, 600) overlay.Fill(color.RGBA{0, 0, 0, 150}) screen.DrawImage(overlay, &ebiten.DrawImageOptions{}) ebitenutil.DebugPrintAt(screen, "--- PAUSAD ---", 350, 200) options := []string{"Fortsatt", "Installningar"} for i, opt := range options { prefix := " " if i == s.selectedIndex { prefix = "> " } ebitenutil.DebugPrintAt(screen, prefix+opt, 350, 250+(i*30)) } } func (s *PlayScene) drawSettingsUI(screen *ebiten.Image) { overlay := ebiten.NewImage(800, 600) overlay.Fill(color.RGBA{0, 0, 0, 200}) screen.DrawImage(overlay, &ebiten.DrawImageOptions{}) ebitenutil.DebugPrintAt(screen, "--- INSTALLNINGAR ---", 300, 30) if input.Manager.IsRecording() { ebitenutil.DebugPrintAt(screen, ">> SPELAR IN: HÅLL IN TANGENTER OCH TRYCK ENTER FOR ATT SPARA <<", 180, 60) ebitenutil.DebugPrintAt(screen, "ESC for att avbryta", 300, 80) } else { ebitenutil.DebugPrintAt(screen, "Tryck Enter for att ga in i redigeringslage, ESC for att abryta", 180, 60) } startY := 120 for i, action := range s.sortedActions { prefix := " " if i == s.selectedIndex { prefix = "> " } // Bygg en sträng för att visa nuvarande tangenter bindingStr := "" bindings := input.Manager.Actions[action] if input.Manager.IsRecording() && input.Manager.GetRecordingAction() == action { // Visar vilka knappar som spelas in just nu recKeys := input.Manager.GetRecordingKeys() if len(recKeys) == 0 { bindingStr = "[Vantar pa dig...]" } else { names := []string{} for _, rk := range recKeys { names = append(names, input.KeyName(rk)) } bindingStr = fmt.Sprintf("[TRYCK ENTER FOR ATT SPARA: %v]", names) } } else { if len(bindings) > 0 { for bi, b := range bindings { if bi > 0 { bindingStr += " ELLER " } names := []string{} for _, k := range b { names = append(names, input.KeyName(k)) } bindingStr += fmt.Sprintf("%v", names) } } else { bindingStr = "[Ingen]" } } line := fmt.Sprintf("%s%-15s : %s", prefix, action, bindingStr) ebitenutil.DebugPrintAt(screen, line, 100, startY+(i*25)) } }