commit 352c7af7ce8a2c45d01e23b9f5065e3db0b6e374 Author: Bruno Rybársky Date: Thu Aug 29 00:05:28 2024 +0200 Init diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5a04147 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tunelar.iml b/.idea/tunelar.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/tunelar.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/base.go b/base.go new file mode 100644 index 0000000..cc39ae4 --- /dev/null +++ b/base.go @@ -0,0 +1,144 @@ +package main + +import "github.com/veandco/go-sdl2/sdl" + +const ( + openingWidth = 7 +) + +type Base struct { + rect sdl.Rect + owner *Player +} + +func (base *Base) tick(players *[]*Player) { + for _, player := range *players { + if player.cullingRect.HasIntersection(&base.rect) { + if player.rechargeCooldown == 0 && player.energy < MaxEnergy { + player.energy++ + if player == base.owner { + player.rechargeCooldown = RechargeCooldownOwn + } else { + player.rechargeCooldown = RechargeCooldownOpponent + } + } + if player == base.owner && player.repairCooldown == 0 && player.health < MaxHealth { + player.health++ + player.repairCooldown = RepairCooldown + } + } + } +} + +func (base *Base) render(camera *sdl.Rect, surface *sdl.Surface) { + borderWidth := base.rect.W - openingWidth + borderWidthA := borderWidth / 2 + borderWidthB := borderWidth - borderWidthA + 1 + if borderWidth < 0 { + panic("Bad border width") + } + rects := []sdl.Rect{ + { + X: base.rect.X, + Y: base.rect.Y, + W: 1, + H: base.rect.H, + }, + { + X: base.rect.X + base.rect.W, + Y: base.rect.Y, + W: 1, + H: base.rect.H, + }, + { + X: base.rect.X, + Y: base.rect.Y, + W: borderWidthA, + H: 1, + }, + { + X: base.rect.X + borderWidthA + openingWidth, + Y: base.rect.Y, + W: borderWidthB, + H: 1, + }, + { + X: base.rect.X, + Y: base.rect.Y + base.rect.H, + W: borderWidthA, + H: 1, + }, + { + X: base.rect.X + borderWidthA + openingWidth, + Y: base.rect.Y + base.rect.H, + W: borderWidthB, + H: 1, + }, + } + + for _, rect := range rects { + if camera.HasIntersection(&rect) { + cameraCompensatedRect := sdl.Rect{ + X: rect.X - camera.X, + Y: rect.Y - camera.Y, + W: rect.W, + H: rect.H, + } + surface.FillRect(&cameraCompensatedRect, (*base.owner).color2) + } + } +} + +func (base *Base) build(gameMap *GameMap) { + borderWidth := base.rect.W - openingWidth + borderWidthA := borderWidth / 2 + if borderWidth < 0 { + panic("Bad border width") + } + if base.rect.H < 9 { + panic("Bad border height") + } + if gameMap.width-base.rect.X-base.rect.W <= 0 { + panic("Bad base x location") + } + if gameMap.height-base.rect.Y-base.rect.H <= 0 { + panic("Bad base y location") + } + if base.rect.X < 0 || base.rect.Y < 0 { + panic("Bad base negative location") + } + for x := base.rect.X; x < base.rect.X+base.rect.W+1; x++ { + for y := base.rect.Y; y < base.rect.Y+base.rect.H+1; y++ { + gameMap.tiles[x][y] = 0 + } + } + for y := base.rect.Y; y < base.rect.Y+base.rect.H; y++ { + gameMap.tiles[base.rect.X][y] = 4 + gameMap.tiles[base.rect.X+base.rect.W][y] = 4 + } + for x := base.rect.X; x < base.rect.X+borderWidthA; x++ { + gameMap.tiles[x][base.rect.Y] = 4 + gameMap.tiles[x][base.rect.Y+base.rect.H] = 4 + } + for x := base.rect.X + borderWidthA + openingWidth; x < base.rect.X+base.rect.W+1; x++ { + gameMap.tiles[x][base.rect.Y] = 4 + gameMap.tiles[x][base.rect.Y+base.rect.H] = 4 + } +} + +func createBases(players *[]*Player, gameMap *GameMap) *[]*Base { + bases := &[]*Base{} + for ownerID, player := range *players { + *bases = append(*bases, &Base{ + rect: sdl.Rect{ + X: player.posX - 14, + Y: player.posY - 14, + W: 35, + H: 35, + }, + owner: (*players)[ownerID], + }) + (*bases)[ownerID].build(gameMap) + } + return bases +} diff --git a/bullet.go b/bullet.go new file mode 100644 index 0000000..112472c --- /dev/null +++ b/bullet.go @@ -0,0 +1,172 @@ +package main + +import ( + "github.com/veandco/go-sdl2/sdl" + "image/color" + "math/rand" +) + +type Bullet struct { + posX, posY int32 + direction uint8 + color color.Color + super bool +} + +type BulletParticle struct { + posX, posY int32 + expirationTimer uint8 + color color.Color +} + +func (bulletParticle *BulletParticle) render(camera *sdl.Rect, surface *sdl.Surface) { + if camera.IntersectLine(&bulletParticle.posX, &bulletParticle.posY, &bulletParticle.posX, &bulletParticle.posY) { + relativeBulletX := bulletParticle.posX - camera.X + relativeBulletY := bulletParticle.posY - camera.Y + surface.Set(int(relativeBulletX), int(relativeBulletY), bulletParticle.color) + } +} + +func (bulletParticle *BulletParticle) tick(particles *[]*BulletParticle) { + if bulletParticle.expirationTimer <= 0 { + for i, particle := range *particles { + if particle == bulletParticle { + *particles = append((*particles)[:i], (*particles)[i+1:]...) + } + } + } + bulletParticle.expirationTimer-- +} + +func (bullet *Bullet) render(camera *sdl.Rect, surface *sdl.Surface) { + if camera.IntersectLine(&bullet.posX, &bullet.posY, &bullet.posX, &bullet.posY) { + relativeBulletX := bullet.posX - camera.X + relativeBulletY := bullet.posY - camera.Y + surface.Set(int(relativeBulletX), int(relativeBulletY), bullet.color) + } +} + +func (bullet *Bullet) explode(gameMap *GameMap, bulletParticleMap *[]*BulletParticle) { + // Define the possible directions with their x and y velocity components. + directions := [][2]int{ + {0, -1}, // Up + {0, 1}, // Down + {-1, 0}, // Left + {1, 0}, // Right + {-1, -1}, // Up-Left Diagonal + {1, -1}, // Up-Right Diagonal + {-1, 1}, // Down-Left Diagonal + {1, 1}, // Down-Right Diagonal + } + + if !bullet.super { + // Loop through each direction to generate particle streams. + for _, dir := range directions { + // Randomize the number of particles in each stream. + particlesInStream := rand.Intn(5) + for i := 0; i < particlesInStream; i++ { + // Determine particle's position based on its direction and step count. + step := rand.Intn(3) + 1 // Step can be 1, 2, or 3 units. + xOffset := dir[0] * step + yOffset := dir[1] * step + + xPos, yPos := bullet.posX+int32(xOffset), bullet.posY+int32(yOffset) + collision := gameMap.checkCollision(xPos, yPos) + + // Check for collision and bullet behavior (normal or super). + if collision == 1 || collision == 0 || (bullet.super && collision == 2) { + *bulletParticleMap = append(*bulletParticleMap, &BulletParticle{ + posX: xPos, + posY: yPos, + expirationTimer: uint8(5 + rand.Intn(10)), // Randomize expiration time between 15 and 25. + color: bullet.color, + }) + if xPos > 0 && yPos > 0 && xPos < MapWidth && yPos < MapHeight { + gameMap.tiles[xPos][yPos] = 0 + } + } + } + } + } else { + for xOffset := -3; xOffset <= 3; xOffset++ { + for yOffset := -3; yOffset <= 3; yOffset++ { + xPos, yPos := bullet.posX+int32(xOffset), bullet.posY+int32(yOffset) + collision := gameMap.checkCollision(xPos, yPos) + if collision == 1 || collision == 0 || (bullet.super && collision == 2) { + *bulletParticleMap = append(*bulletParticleMap, &BulletParticle{ + posX: xPos, + posY: yPos, + expirationTimer: uint8(15 + rand.Intn(15)), + color: bullet.color, + }) + gameMap.tiles[xPos][yPos] = 0 + } + } + } + } +} + +func (bullet *Bullet) tick(gameMap *GameMap, + bulletParticleMap *[]*BulletParticle, bulletMap *[]*Bullet, players *[]*Player) { + var nextX, nextY int32 + nextX, nextY = bullet.posX, bullet.posY + switch bullet.direction { + case 0: //up + nextY -= 1 + break + case 1: //right + nextX += 1 + break + case 2: //down + nextY += 1 + break + case 3: //left + nextX -= 1 + break + case 4: //up right + nextY -= 1 + nextX += 1 + break + case 5: //up left + nextY -= 1 + nextX -= 1 + break + case 6: //down right + nextY += 1 + nextX += 1 + break + case 7: //down left + nextY += 1 + nextX -= 1 + break + } + collisionResult := gameMap.checkCollision(nextX, nextY) + bulletRect := sdl.Rect{ + X: bullet.posX, + Y: bullet.posY, + W: 1, + H: 1, + } + hitPlayer := false + for _, player := range *players { + if player.rect1.HasIntersection(&bulletRect) || + player.rect2.HasIntersection(&bulletRect) || + player.rect3.HasIntersection(&bulletRect) || + player.rect4.HasIntersection(&bulletRect) { + hitPlayer = true + player.health -= 20 + break + } + } + if collisionResult != 0 || hitPlayer { + bullet.explode(gameMap, bulletParticleMap) + for i, bulletInMap := range *bulletMap { + if bulletInMap == bullet { + *bulletMap = append((*bulletMap)[:i], (*bulletMap)[i+1:]...) + } + } + } else { + bullet.posX = nextX + bullet.posY = nextY + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cf8b4a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module goingtunneling + +go 1.21 + +require ( + github.com/aquilax/go-perlin v1.1.0 + github.com/veandco/go-sdl2 v0.4.40 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..24c3d7e --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/aquilax/go-perlin v1.1.0 h1:Gg+3jQ24wT4Y5GI7TCRLmYarzUG0k+n/JATFqOimb7s= +github.com/aquilax/go-perlin v1.1.0/go.mod h1:z9Rl7EM4BZY0Ikp2fEN1I5mKSOJ26HQpk0O2TBdN2HE= +github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U= +github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= diff --git a/graphics.go b/graphics.go new file mode 100644 index 0000000..ef5e309 --- /dev/null +++ b/graphics.go @@ -0,0 +1,156 @@ +package main + +import ( + "github.com/veandco/go-sdl2/sdl" +) + +func initializeSDL() { + if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + panic(err) + } +} + +func setupWindowAndSurface() (*sdl.Window, *sdl.Surface) { + const windowWidth, windowHeight = 160, 100 + window, err := sdl.CreateWindow("test", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, windowWidth, windowHeight, sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE) + if err != nil { + panic(err) + } + + logicalSurface, err := sdl.CreateRGBSurface(0, windowWidth, windowHeight, 32, 0, 0, 0, 0) + if err != nil { + panic(err) + } + + return window, logicalSurface +} + +func setupPlaySurface() (*sdl.Surface, *sdl.Rect, *sdl.Rect) { + playSurface, err := sdl.CreateRGBSurface(0, 76, 76, 32, 0, 0, 0, 0) + if err != nil { + panic(err) + } + playSurfaceRect := &sdl.Rect{X: 0, Y: 0, W: 76, H: 76} + playSurfaceTargetRect := &sdl.Rect{X: 42, Y: 0, W: 76, H: 76} + + return playSurface, playSurfaceRect, playSurfaceTargetRect +} + +func setupHUDSurface() (*sdl.Surface, *sdl.Rect, *sdl.Rect) { + HUDSurface, err := sdl.CreateRGBSurface(0, 112, 25, 32, 0, 0, 0, 0) + if err != nil { + panic(err) + } + HUDSurfaceRect := &sdl.Rect{X: 0, Y: 0, W: 112, H: 25} + HUDSurfaceTargetRect := &sdl.Rect{X: 24, Y: 76, W: 112, H: 25} + + return HUDSurface, HUDSurfaceRect, HUDSurfaceTargetRect +} + +func handleEvents(window *sdl.Window, logicalSurface *sdl.Surface) bool { + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch e := event.(type) { + case *sdl.QuitEvent: + return false + case *sdl.WindowEvent: + if e.Event == sdl.WINDOWEVENT_RESIZED { + handleWindowResize(window, logicalSurface) + } + } + } + return true +} + +func handleWindowResize(window *sdl.Window, logicalSurface *sdl.Surface) { + windowSurface, err := window.GetSurface() + if err != nil { + panic(err) + } + + windowWidth, windowHeight := windowSurface.W, windowSurface.H + + aspectRatio := float64(logicalSurface.W) / float64(logicalSurface.H) + newWidth := windowWidth + newHeight := int32(float64(windowWidth) / aspectRatio) + + if newHeight > windowHeight { + newHeight = windowHeight + newWidth = int32(float64(windowHeight) * aspectRatio) + } + + letterboxX := (windowWidth - newWidth) / 2 + letterboxY := (windowHeight - newHeight) / 2 + + windowSurface.FillRect(nil, 0) + srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H} + dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight} + + logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) + window.UpdateSurface() +} + +func renderScene(logicalSurface, playSurface, HUDSurface *sdl.Surface, gameMap *GameMap, players *[]*Player, bases *[]*Base, bullets *[]*Bullet, bulletParticles *[]*BulletParticle, camera *sdl.Rect, playSurfaceRect, playSurfaceTargetRect, HUDSurfaceRect, HUDSurfaceTargetRect *sdl.Rect) { + HUDColor := sdl.MapRGBA(HUDSurface.Format, 101, 101, 101, 255) + logicalColor := sdl.MapRGBA(HUDSurface.Format, 101, 101, 0, 255) + playColor := sdl.MapRGBA(HUDSurface.Format, 101, 0, 101, 255) + + logicalSurface.FillRect(nil, logicalColor) + playSurface.FillRect(nil, playColor) + HUDSurface.FillRect(nil, HUDColor) + + (*players)[0].track(camera) + + gameMap.render(camera, playSurface) + + for _, bullet := range *bullets { + (*bullet).render(camera, playSurface) + (*bullet).tick(gameMap, bulletParticles, bullets, players) + } + + for _, base := range *bases { + (*base).render(camera, playSurface) + (*base).tick(players) + } + + for _, player := range *players { + (*player).render(camera, playSurface) + (*player).tick() + } + + for _, bulletParticle := range *bulletParticles { + bulletParticle.render(camera, playSurface) + bulletParticle.tick(bulletParticles) + } + + renderHud((*players)[0], HUDSurface) + + playSurface.BlitScaled(playSurfaceRect, logicalSurface, playSurfaceTargetRect) + HUDSurface.BlitScaled(HUDSurfaceRect, logicalSurface, HUDSurfaceTargetRect) +} + +func adjustWindow(window *sdl.Window, logicalSurface *sdl.Surface, pixelBG uint32) { + windowSurface, err := window.GetSurface() + if err != nil { + panic(err) + } + + windowWidth, windowHeight := windowSurface.W, windowSurface.H + aspectRatio := float64(logicalSurface.W) / float64(logicalSurface.H) + newWidth := windowWidth + newHeight := int32(float64(windowWidth) / aspectRatio) + + if newHeight > windowHeight { + newHeight = windowHeight + newWidth = int32(float64(windowHeight) * aspectRatio) + } + + letterboxX := (windowWidth - newWidth) / 2 + letterboxY := (windowHeight - newHeight) / 2 + + windowSurface.FillRect(nil, pixelBG) + srcRect := &sdl.Rect{X: 0, Y: 0, W: logicalSurface.W, H: logicalSurface.H} + dstRect := &sdl.Rect{X: letterboxX, Y: letterboxY, W: newWidth, H: newHeight} + + logicalSurface.BlitScaled(srcRect, windowSurface, dstRect) + window.UpdateSurface() +} diff --git a/hud.go b/hud.go new file mode 100644 index 0000000..55fbf47 --- /dev/null +++ b/hud.go @@ -0,0 +1,78 @@ +package main + +import "github.com/veandco/go-sdl2/sdl" + +func renderHud(player *Player, surface *sdl.Surface) { + eRects := []sdl.Rect{ + { + X: 5, + Y: 4, + W: 6, + H: 1, + }, + { + X: 5, + Y: 4, + W: 1, + H: 5, + }, + { + X: 5, + Y: 6, + W: 6, + H: 1, + }, + { + X: 5, + Y: 8, + W: 6, + H: 1, + }, + } + + sRects := []sdl.Rect{ + { + X: 5, + Y: 18, + W: 6, + H: 1, + }, + { + X: 5, + Y: 18, + W: 1, + H: 3, + }, + { + X: 5, + Y: 20, + W: 6, + H: 1, + }, + { + X: 10, + Y: 20, + W: 1, + H: 3, + }, + { + X: 5, + Y: 22, + W: 6, + H: 1, + }, + } + + for _, rect := range eRects { + surface.FillRect(&rect, sdl.MapRGBA(surface.Format, 243, 235, 28, 255)) + } + for _, rect := range sRects { + surface.FillRect(&rect, sdl.MapRGBA(surface.Format, 40, 243, 243, 255)) + } + + surface.FillRect(&sdl.Rect{X: 16, Y: 3, W: 90, H: 7}, sdl.MapRGBA(surface.Format, 0, 0, 0, 255)) + surface.FillRect(&sdl.Rect{X: 16, Y: 17, W: 90, H: 7}, sdl.MapRGBA(surface.Format, 0, 0, 0, 255)) + + surface.FillRect(&sdl.Rect{X: 17, Y: 4, W: int32(player.energy / 20), H: 5}, sdl.MapRGBA(surface.Format, 243, 235, 28, 255)) + surface.FillRect(&sdl.Rect{X: 17, Y: 18, W: int32(player.health), H: 5}, sdl.MapRGBA(surface.Format, 40, 243, 243, 255)) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c12c292 --- /dev/null +++ b/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "github.com/veandco/go-sdl2/sdl" +) + +const ( + MapWidth = 1000 // Width of the map + MapHeight = 1000 // Height of the map + MaxEnergy = 1760 + MaxHealth = 88 + NormalShotCost = 3 + SuperShotCost = 20 + MovementCost = 1 + DiggingCost = 2 + ShootDiggingCostBonus = 1 + ShootCooldown = 8 + RechargeCooldownOwn = 0 + DiggingCooldown = 4 + RechargeCooldownOpponent = 6 + RepairCooldown = 4 + MovementCooldown = 2 + MovementCooldownNoEnergy = 4 + DiggingCooldownNoEnergy = 8 +) + +func main() { + initializeSDL() + defer sdl.Quit() + + window, logicalSurface := setupWindowAndSurface() + window.SetTitle("GOing to tunnel") + defer window.Destroy() + defer logicalSurface.Free() + + playSurface, playSurfaceRect, playSurfaceTargetRect := setupPlaySurface() + defer playSurface.Free() + + HUDSurface, HUDSurfaceRect, HUDSurfaceTargetRect := setupHUDSurface() + defer HUDSurface.Free() + + pixelBG := sdl.MapRGBA(logicalSurface.Format, 80, 20, 10, 255) + + gameMap := GameMap{} + gameMap.createGameMap(MapWidth, MapHeight) + + players := &[]*Player{} + + createPlayer( + sdl.MapRGBA(playSurface.Format, 0, 0, 182, 255), + sdl.MapRGBA(playSurface.Format, 44, 44, 255, 255), + sdl.MapRGBA(playSurface.Format, 243, 235, 28, 255), + &gameMap, + players) + + createPlayer( + sdl.MapRGBA(playSurface.Format, 44, 184, 44, 255), + sdl.MapRGBA(playSurface.Format, 0, 249, 0, 255), + sdl.MapRGBA(playSurface.Format, 243, 235, 28, 255), + &gameMap, + players) + + bases := createBases(players, &gameMap) + + bulletMap := &[]*Bullet{} + bulletParticleMap := &[]*BulletParticle{} + + camera := sdl.Rect{X: 0, Y: 0, W: 76, H: 76} + + running := true + for running { + currentTime := sdl.GetTicks64() + + running = handleEvents(window, logicalSurface) + + keyboard := sdl.GetKeyboardState() + + running = running && handleInput(keyboard, bulletMap, (*players)[0], &gameMap, playSurface.Format, players) + + renderScene(logicalSurface, playSurface, HUDSurface, &gameMap, players, bases, bulletMap, bulletParticleMap, &camera, playSurfaceRect, playSurfaceTargetRect, HUDSurfaceRect, HUDSurfaceTargetRect) + + adjustWindow(window, logicalSurface, pixelBG) + + // Calculate delay to achieve roughly 60 FPS + frameDuration := 1000 / 60 + elapsed := sdl.GetTicks64() - currentTime + if delay := frameDuration - int(elapsed); delay > 0 { + sdl.Delay(uint32(delay)) + } + } +} + +func handleInput(keyboard []uint8, bullets *[]*Bullet, player *Player, gameMap *GameMap, format *sdl.PixelFormat, players *[]*Player) bool { + shoot := false + super := false + + // Flags to track movement in each direction + moveUp := false + moveDown := false + moveLeft := false + moveRight := false + + // Process keyboard input + for key, value := range keyboard { + if value == 0 { + continue + } + + if key == sdl.SCANCODE_ESCAPE { + return false + } else if key == sdl.SCANCODE_SPACE { + shoot = true + } else if key == sdl.SCANCODE_LCTRL { + super = true + } else if key == sdl.SCANCODE_W { + moveUp = true + } else if key == sdl.SCANCODE_S { + moveDown = true + } else if key == sdl.SCANCODE_A { + moveLeft = true + } else if key == sdl.SCANCODE_D { + moveRight = true + } + } + + // Handle shooting after the loop + if shoot { + player.shoot(super, bullets, format) + } + + // Determine player orientation for diagonal movement + if moveUp && moveRight { + player.orientation = 4 // Up-Right + } else if moveUp && moveLeft { + player.orientation = 5 // Up-Left + } else if moveDown && moveRight { + player.orientation = 6 // Down-Right + } else if moveDown && moveLeft { + player.orientation = 7 // Down-Left + } else if moveUp { + player.orientation = 0 // Up + } else if moveRight { + player.orientation = 1 // Right + } else if moveDown { + player.orientation = 2 // Down + } else if moveLeft { + player.orientation = 3 // Left + } + + // Handle movement after the loop + if moveUp || moveDown || moveLeft || moveRight { + if player.tryMove(gameMap, shoot, players) { + player.energy -= MovementCost + } + } + + return true +} diff --git a/map.go b/map.go new file mode 100644 index 0000000..6cc7cf8 --- /dev/null +++ b/map.go @@ -0,0 +1,116 @@ +package main + +import ( + "github.com/aquilax/go-perlin" + "github.com/veandco/go-sdl2/sdl" + "image/color" + "time" +) + +type GameMap struct { + tiles [][]uint8 + //tile 0 - air + //tile 1 - rock + //tile 2 - soil 1 + //tile 3 - soil 2 + //tile 4 - base + //tile 5 - world edge (should not be present in the map object) + width int32 + height int32 +} + +func GetTileColorValue(value uint8) color.Color { + switch value { + case 0: + return color.RGBA{} + case 1: + return color.RGBA{R: 154, G: 154, B: 154, A: 255} + case 2: + return color.RGBA{R: 195, G: 121, B: 48, A: 255} + case 3: + return color.RGBA{R: 186, G: 89, B: 4, A: 255} + case 4: + return color.RGBA{R: 255, G: 44, B: 255, A: 255} + case 5: + return color.RGBA{R: 86, G: 86, B: 86, A: 255} + default: + return color.RGBA{R: 255, G: 255, B: 255, A: 255} + } +} + +func (gameMap *GameMap) getTileColor(x int32, y int32) color.Color { + if x < 0 || x >= gameMap.width || y < 0 || y >= gameMap.height { + return GetTileColorValue(5) + } else { + tileValue := gameMap.tiles[x][y] + return GetTileColorValue(tileValue) + } +} + +func (gameMap *GameMap) createGameMap(width int32, height int32) { + gameMap.tiles = make([][]uint8, width) + perlinNoiseRock := perlin.NewPerlin(2, 2, 4, time.Now().Unix()) + perlinNoiseSoil := perlin.NewPerlin(2, 8, 4, time.Now().Unix()) + for i := range gameMap.tiles { + gameMap.tiles[i] = make([]uint8, height) + for j := range gameMap.tiles[i] { + perlinRock := perlinNoiseRock.Noise2D(float64(i)/float64(width)*3, float64(j)/float64(height)*3) + if perlinRock > 0.2 { + gameMap.tiles[i][j] = 1 + } else { + perlinSoil := perlinNoiseSoil.Noise2D(float64(i)/float64(width)*10, float64(j)/float64(height)*10) + if perlinSoil >= 0 { + gameMap.tiles[i][j] = 2 + } else { + gameMap.tiles[i][j] = 3 + } + } + + } + } + gameMap.width = width + gameMap.height = height +} + +func (gameMap *GameMap) render(camera *sdl.Rect, surface *sdl.Surface) { + fromX := camera.X + fromY := camera.Y + toX := camera.X + camera.W + toY := camera.Y + camera.H + for x := fromX; x < toX; x++ { + for y := fromY; y < toY; y++ { + cameraY := y - camera.Y + cameraX := x - camera.X + pixelColor := gameMap.getTileColor(x, y) + surface.Set(int(cameraX), int(cameraY), pixelColor) + + } + } +} + +func (gameMap *GameMap) checkCollision(posX, posY int32) uint8 { + //0 no collision + //1 destructible collision + //2 rock collision + //3 indestructible collision + if posX >= gameMap.width || posX < 0 || posY >= gameMap.height || posY < 0 { + return 3 + } + switch gameMap.tiles[posX][posY] { + case 0: + return 0 + case 1: + return 2 + case 2: + return 1 + case 3: + return 1 + case 4: + return 3 + case 5: + return 3 + default: + return 3 + } + +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..e6b5483 --- /dev/null +++ b/player.go @@ -0,0 +1,528 @@ +package main + +import ( + "github.com/veandco/go-sdl2/sdl" + "image/color" + "math/rand" +) + +type Player struct { + posX int32 + posY int32 + orientation uint8 + color1 uint32 + color2 uint32 + color3 uint32 + energy uint16 + health uint16 + digCooldown uint8 + shootCooldown uint8 + repairCooldown uint8 + rechargeCooldown uint8 + movementCooldown uint8 + rect1 sdl.Rect + rect2 sdl.Rect + rect3 sdl.Rect + rect4 sdl.Rect + cullingRect sdl.Rect +} + +func (player *Player) track(camera *sdl.Rect) { + camera.X = player.posX - 37 + camera.Y = player.posY - 38 +} + +func (player *Player) render(camera *sdl.Rect, surface *sdl.Surface) { + relativePlayerX := player.posX - camera.X + relativePlayerY := player.posY - camera.Y + + switch player.orientation { + case 0: // Up + player.rect1 = sdl.Rect{X: relativePlayerX, Y: relativePlayerY + 1, W: 1, H: 6} + player.rect2 = sdl.Rect{X: relativePlayerX + 4, Y: relativePlayerY + 1, W: 1, H: 6} + player.rect3 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 2, W: 3, H: 4} + player.rect4 = sdl.Rect{X: relativePlayerX + 2, Y: relativePlayerY, W: 1, H: 4} + + case 1: // Right + player.rect1 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 1, W: 6, H: 1} + player.rect2 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 5, W: 6, H: 1} + player.rect3 = sdl.Rect{X: relativePlayerX + 2, Y: relativePlayerY + 2, W: 4, H: 3} + player.rect4 = sdl.Rect{X: relativePlayerX + 4, Y: relativePlayerY + 3, W: 4, H: 1} + + case 2: // Down + player.rect1 = sdl.Rect{X: relativePlayerX, Y: relativePlayerY + 1, W: 1, H: 6} + player.rect2 = sdl.Rect{X: relativePlayerX + 4, Y: relativePlayerY + 1, W: 1, H: 6} + player.rect3 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 2, W: 3, H: 4} + player.rect4 = sdl.Rect{X: relativePlayerX + 2, Y: relativePlayerY + 4, W: 1, H: 4} + + case 3: // Left + player.rect1 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 1, W: 6, H: 1} + player.rect2 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 5, W: 6, H: 1} + player.rect3 = sdl.Rect{X: relativePlayerX + 2, Y: relativePlayerY + 2, W: 4, H: 3} + player.rect4 = sdl.Rect{X: relativePlayerX, Y: relativePlayerY + 3, W: 4, H: 1} + + case 4: // Up-Right + player.rect1 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 6, W: 5, H: 1} + player.rect2 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 2, W: 1, H: 4} + player.rect3 = sdl.Rect{X: relativePlayerX + 2, Y: relativePlayerY + 2, W: 4, H: 4} + player.rect4 = sdl.Rect{X: relativePlayerX + 4, Y: relativePlayerY + 2, W: 2, H: 2} + + case 5: // Up-Left + player.rect1 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 6, W: 5, H: 1} + player.rect2 = sdl.Rect{X: relativePlayerX + 5, Y: relativePlayerY + 2, W: 1, H: 5} + player.rect3 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 2, W: 4, H: 4} + player.rect4 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 2, W: 2, H: 2} + + case 6: // Down-Right + player.rect1 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 1, W: 5, H: 1} + player.rect2 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 1, W: 1, H: 5} + player.rect3 = sdl.Rect{X: relativePlayerX + 2, Y: relativePlayerY + 2, W: 4, H: 4} + player.rect4 = sdl.Rect{X: relativePlayerX + 4, Y: relativePlayerY + 4, W: 2, H: 2} + + case 7: // Down-Left + player.rect1 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 1, W: 4, H: 1} + player.rect2 = sdl.Rect{X: relativePlayerX + 5, Y: relativePlayerY + 1, W: 1, H: 5} + player.rect3 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 2, W: 4, H: 4} + player.rect4 = sdl.Rect{X: relativePlayerX + 1, Y: relativePlayerY + 4, W: 2, H: 2} + } + + // Common part: Rendering player + player.cullingRect = sdl.Rect{X: player.posX, Y: player.posY, W: 7, H: 7} + cameraAdjustedCullingRect := sdl.Rect{ + X: player.cullingRect.X - camera.X, + Y: player.cullingRect.Y - camera.Y, + W: player.cullingRect.W, + H: player.cullingRect.H, + } + cameraAdjustedCullingRectBorder := sdl.Rect{ + X: player.cullingRect.X - camera.X - 1, + Y: player.cullingRect.Y - camera.Y - 1, + W: player.cullingRect.W + 2, + H: player.cullingRect.H + 2, + } + if camera.HasIntersection(&player.cullingRect) { + surface.FillRect(&cameraAdjustedCullingRectBorder, sdl.MapRGBA(surface.Format, 10, 255, 255, 64)) + surface.FillRect(&cameraAdjustedCullingRect, sdl.MapRGBA(surface.Format, 255, 20, 10, 64)) + surface.FillRect(&player.rect1, player.color1) + surface.FillRect(&player.rect2, player.color1) + surface.FillRect(&player.rect3, player.color2) + surface.FillRect(&player.rect4, player.color3) + } +} + +func (player *Player) digBlock(posX, posY int32, gameMap *GameMap, isShooting bool) uint8 { + collisionAtDigSite := gameMap.checkCollision(posX, posY) + if collisionAtDigSite == 0 { + return 0 + } else if collisionAtDigSite == 1 && (player.digCooldown == 0 || isShooting) { + gameMap.tiles[posX][posY] = 0 + player.digCooldown = DiggingCooldown + return 1 + } + return 2 +} + +func (player *Player) tick() { + if player.digCooldown > 0 { + player.digCooldown-- + } + if player.shootCooldown > 0 { + player.shootCooldown-- + } + if player.repairCooldown > 0 { + player.repairCooldown-- + } + if player.rechargeCooldown > 0 { + player.rechargeCooldown-- + } + if player.movementCooldown > 0 { + player.movementCooldown-- + } +} + +func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players *[]*Player) (moved bool) { + if player.movementCooldown > 0 { + return false + } + ranOutOfEnergy := (isShooting && player.energy <= DiggingCost+ShootDiggingCostBonus) || player.energy <= DiggingCost + if ranOutOfEnergy { + isShooting = false + } + shouldPenalizeDigging := false + stopped := false + + switch player.orientation { + case 0: // Up + collisionRect := sdl.Rect{ + X: player.posX, + Y: player.posY - 1, + W: 5, + H: 7, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for x := player.posX; x < player.posX+5 && !stopped; x++ { + for y := player.posY - 1; y < player.posY+7; y++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posY-- + } + + case 1: // Right + collisionRect := sdl.Rect{ + X: player.posX + 1, + Y: player.posY, + W: 7, + H: 5, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for y := player.posY; y < player.posY+7 && !stopped; y++ { + for x := player.posX + 1; x < player.posX+8; x++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posX++ + } + + case 2: // Down + collisionRect := sdl.Rect{ + X: player.posX, + Y: player.posY + 1, + W: 5, + H: 7, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for x := player.posX; x < player.posX+5 && !stopped; x++ { + for y := player.posY + 1; y < player.posY+8; y++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posY++ + } + + case 3: // Left + collisionRect := sdl.Rect{ + X: player.posX - 1, + Y: player.posY, + W: 7, + H: 5, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for y := player.posY; y < player.posY+6 && !stopped; y++ { + for x := player.posX - 1; x < player.posX+6; x++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posX-- + } + + case 4: // Up-Right + collisionRect := sdl.Rect{ + X: player.posX + 1, + Y: player.posY - 1, + W: 7, + H: 7, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for x := player.posX + 1; x < player.posX+8 && !stopped; x++ { + for y := player.posY - 1; y < player.posY+7; y++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posX++ + player.posY-- + } + + case 5: // Up-Left + collisionRect := sdl.Rect{ + X: player.posX - 1, + Y: player.posY - 1, + W: 7, + H: 7, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for x := player.posX - 1; x < player.posX+6 && !stopped; x++ { + for y := player.posY - 1; y < player.posY+7; y++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posX-- + player.posY-- + } + + case 6: // Down-Right + collisionRect := sdl.Rect{ + X: player.posX + 1, + Y: player.posY + 1, + W: 7, + H: 7, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for x := player.posX + 1; x < player.posX+8 && !stopped; x++ { + for y := player.posY + 1; y < player.posY+8; y++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posX++ + player.posY++ + } + + case 7: // Down-Left + collisionRect := sdl.Rect{ + X: player.posX - 1, + Y: player.posY + 1, + W: 7, + H: 7, + } + for _, opponent := range *players { + if opponent != player && opponent.cullingRect.HasIntersection(&collisionRect) { + return false + } + } + for x := player.posX - 1; x < player.posX+6 && !stopped; x++ { + for y := player.posY + 1; y < player.posY+8; y++ { + digResult := player.digBlock(x, y, gameMap, isShooting) + if digResult == 1 { + shouldPenalizeDigging = true + if !isShooting { + stopped = true + break + } + } else if digResult != 0 { + stopped = true + break + } + } + } + if !stopped { + moved = true + player.posX-- + player.posY++ + } + } + + // Penalties and cooldown handling + if shouldPenalizeDigging { + if ranOutOfEnergy { + player.digCooldown = DiggingCooldownNoEnergy + } + player.energy -= DiggingCost + if isShooting { + player.energy -= ShootDiggingCostBonus + } + } + if moved { + if ranOutOfEnergy { + player.movementCooldown = MovementCooldownNoEnergy + } else { + player.movementCooldown = MovementCooldown + } + } + return +} + +func (player *Player) shoot(super bool, bullets *[]*Bullet, format *sdl.PixelFormat) { + if (super && player.energy <= SuperShotCost) || (!super && player.energy <= NormalShotCost) { + return + } + if player.shootCooldown == 0 { + var shootX, shootY int32 + + switch player.orientation { + case 0: // Up + shootY = player.posY - 1 + shootX = player.posX + 2 + break + case 1: // Right + shootY = player.posY + 3 + shootX = player.posX + 8 + break + case 2: // Down + shootX = player.posX + 2 + shootY = player.posY + 8 + break + case 3: // Left + shootY = player.posY + 3 + shootX = player.posX - 2 + break + case 4: // Up-Right + shootY = player.posY - 1 + shootX = player.posX + 5 + break + case 5: // Up-Left + shootY = player.posY - 1 + shootX = player.posX - 1 + break + case 6: // Down-Right + shootY = player.posY + 5 + shootX = player.posX + 5 + break + case 7: // Down-Left + shootY = player.posY + 5 + shootX = player.posX - 1 + break + } + + // Set bullet color + r, g, b, a := sdl.GetRGBA(player.color2, format) + bulletColor := color.RGBA{R: r, G: g, B: b, A: a} + + // Create and add the bullet + *bullets = append(*bullets, &Bullet{ + posX: shootX, + posY: shootY, + direction: player.orientation, + color: bulletColor, + super: super, + }) + + // Set cooldown and decrease energy + player.shootCooldown = ShootCooldown + if super { + player.energy -= SuperShotCost + } else { + player.energy -= NormalShotCost + } + } +} + +func createPlayer(color1 uint32, color2 uint32, color3 uint32, gameMap *GameMap, players *[]*Player) { + coordsAreValid := false + var posX, posY int32 + for !coordsAreValid { + posX = int32(16 + rand.Intn(int(gameMap.width-36))) + posY = int32(16 + rand.Intn(int(gameMap.height-36))) + + coordsAreValid = true + for _, player := range *players { + distance := (player.posX-posX)*(player.posX-posX) + (player.posY-posY)*(player.posY-posY) + if distance < 300*300 { // Check if distance is less than 300 units + coordsAreValid = false + break + } + } + } + + *players = append(*players, &Player{ + posX: posX, + posY: posY, + orientation: 3, + color1: color1, + color2: color2, + color3: color3, + health: MaxHealth, + energy: MaxEnergy, + }) +}