package scenes import ( "image" "image/color" _ "image/jpeg" _ "image/png" "log" "mountain/internal/entities" "github.com/go-gl/mathgl/mgl64" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) const ( Gravity = 0.5 JumpForce = -8.0 MoveSpeed = 4.0 ViewSize = 380 // 800 width, we have 2 viewports (380 each with 20 padding) ) 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 } 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), } } func (s *PlayScene) Update() error { p := s.player currentMoveSpeed := MoveSpeed if ebiten.IsKeyPressed(ebiten.KeyControl) { 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() && ebiten.IsKeyPressed(ebiten.KeyShift) { isClimbing = true } if isClimbing { p.Vel[1] = 0 // Ingen gravitation när man klättrar // W, A, S, D styr X och Y axlar if ebiten.IsKeyPressed(ebiten.KeyA) { p.Vel[0] = -currentMoveSpeed p.FacingRight = false isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyD) { p.Vel[0] = currentMoveSpeed p.FacingRight = true isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyW) { p.Vel[1] = -currentMoveSpeed // Uppåt isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyS) { p.Vel[1] = currentMoveSpeed // Nedåt isMoving = true } // Upp/ner pilarna styr Z-axeln if ebiten.IsKeyPressed(ebiten.KeyUp) { p.Vel[2] = -currentMoveSpeed isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyDown) { p.Vel[2] = currentMoveSpeed isMoving = true } } else { if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) { p.Vel[0] = -currentMoveSpeed p.FacingRight = false isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD) { p.Vel[0] = currentMoveSpeed p.FacingRight = true isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW) { p.Vel[2] = -currentMoveSpeed isMoving = true } if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) { p.Vel[2] = currentMoveSpeed isMoving = true } if ebiten.IsKeyPressed(ebiten.KeySpace) && p.IsGrounded { p.Vel[1] = JumpForce p.IsGrounded = false } if !p.IsGrounded { p.Vel[1] += Gravity } } nextX := p.Pos[0] + p.Vel[0] nextY := p.Pos[1] + p.Vel[1] nextZ := p.Pos[2] + p.Vel[2] gridY := int((nextY + p.Height) / entities.TileSize) gridX := int((nextX + p.Width/2) / entities.TileSize) gridZ := int((nextZ + p.Width/2) / entities.TileSize) entY := s.world.GetEntityAt(gridX, gridY, gridZ) if entY != nil && entY.IsBlocking() && p.Vel[1] >= 0 { nextY = float64(gridY)*entities.TileSize - p.Height - 0.1 p.Vel[1] = 0 p.IsGrounded = true } else { p.IsGrounded = false } p.Pos[0] = nextX p.Pos[1] = nextY p.Pos[2] = nextZ // Kolla ifall vi står på en portal standEnt := s.world.GetEntityAt(gridX, gridY, 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 och "inåt", max 3 djup) // Rendera från djupast till spelarens Z (mindre Z är djupare inåt bild/scen) leftDepthStart := playerGridZ - 3 if leftDepthStart < 0 { leftDepthStart = 0 } for zLevel := leftDepthStart; zLevel <= playerGridZ; zLevel++ { tint := float32(1.0) alpha := float32(1.0) if zLevel < playerGridZ { // Förskugga och gör transparent lager längre "in" diff := float32(playerGridZ - zLevel) tint = 1.0 - (diff * 0.2) alpha = 1.0 - (diff * 0.25) if tint < 0.2 { tint = 0.2 } if alpha < 0.2 { alpha = 0.2 } } 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, tint, alpha) } } } } // Höger vy: Ovan-vy (renderar ner i marken så att det känns som det går "in" i bilden) // -1: Två block inåt jord/hål (Djupast) // 0: Ett block inåt jord/hål // +1: Samma (eller 1 över) // För att saker under spelaren ska ligga bakom byter vi tecken. "upp" i z/y är nu inåt. // Vi visar tre block nedåt i Y: playerGridY+3, Y+2, Y+1, Y topLevels := []int{playerGridY + 3, playerGridY + 2, playerGridY + 1, playerGridY} for i, yLevel := range topLevels { if yLevel >= 0 && yLevel < s.world.Height { tint := float32(1.0) alpha := float32(1.0) // Sätt färger baserat på relativ nivå ("inåt") där de djupaste (Y+3) är mörkare // Index 0 är djupast i marken if i == 0 { // -2 block (långt under fötter) tint = 0.6 alpha = 0.4 } else if i == 1 { // -1 block (nyss under fötter) tint = 0.8 alpha = 0.6 } else if i == 2 { // marken spelaren står på tint = 1.0 alpha = 1.0 } else if i == 3 { // spelarens fothöjd (0) tint = 1.2 alpha = 1.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, tint, alpha) } } } } // Rendera spelaren på toppen (efter marken och spelarens lager är ritat) if i == 3 { s.drawPlayerTop(s.rightView, camX, camZ) } } s.drawPlayerSide(s.leftView, camX, camY) 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)") } 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 op := &ebiten.DrawImageOptions{} drawX := p.Pos[0] - camX - 32 drawZ := p.Pos[2] - camZ - 64 op.GeoM.Translate(drawX, drawZ) screen.DrawImage(p.SpriteTop, op) }