diff options
| author | feurig <don@suspectdevices.com> | 2021-12-05 18:29:18 -0800 |
|---|---|---|
| committer | feurig <don@suspectdevices.com> | 2021-12-05 18:29:18 -0800 |
| commit | cf8db4d4057cbdeeed6bdcb2aa35f19cea4d09f7 (patch) | |
| tree | 5e751ea0099da923ea930db19cb2bcb3ec453818 | |
Steal this framework
| -rw-r--r-- | .gitignore | 21 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | README.md | 19 | ||||
| -rw-r--r-- | go.mod | 8 | ||||
| -rw-r--r-- | go.sum | 6 | ||||
| -rw-r--r-- | images/game_over_screen.png | bin | 0 -> 9816 bytes | |||
| -rw-r--r-- | images/running_game_screen.png | bin | 0 -> 6421 bytes | |||
| -rw-r--r-- | images/title_screen.png | bin | 0 -> 9410 bytes | |||
| -rw-r--r-- | invaders/alien.go | 92 | ||||
| -rw-r--r-- | invaders/alien_cluster.go | 214 | ||||
| -rw-r--r-- | invaders/arena.go | 71 | ||||
| -rw-r--r-- | invaders/entity.go | 10 | ||||
| -rw-r--r-- | invaders/files/alien_basic.txt | 2 | ||||
| -rw-r--r-- | invaders/files/alien_medium.txt | 2 | ||||
| -rw-r--r-- | invaders/files/alien_strong.txt | 2 | ||||
| -rw-r--r-- | invaders/files/game_over.txt | 6 | ||||
| -rw-r--r-- | invaders/files/hero.txt | 2 | ||||
| -rw-r--r-- | invaders/files/title.txt | 6 | ||||
| -rw-r--r-- | invaders/game.go | 217 | ||||
| -rw-r--r-- | invaders/game_over_zone.go | 27 | ||||
| -rw-r--r-- | invaders/hero.go | 104 | ||||
| -rw-r--r-- | invaders/hud.go | 61 | ||||
| -rw-r--r-- | invaders/laser.go | 46 | ||||
| -rw-r--r-- | invaders/screens.go | 97 | ||||
| -rw-r--r-- | invaders/utils/utils.go | 5 | ||||
| -rw-r--r-- | main.go | 7 |
26 files changed, 1046 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..996f1ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Archimedes Fagundes Junior + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8908dda --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Go Invaders +A Space Invaders inspired game written in Go. Built with [Termloop](https://github.com/JoelOtter/termloop). + +## Installation +``` +go install github.com/afagundes/go-invaders@latest +$GOPATH/bin/go-invaders +``` + +## Controls +Use the arrow keys to move the hero spaceship and space bar to shoot. Press backspace to quit. + +# Screenshots + + + + + + @@ -0,0 +1,8 @@ +module github.com/afagundes/go-invaders + +go 1.16 + +require ( + github.com/JoelOtter/termloop v0.0.0-20201118115657-7fa23b4da654 + github.com/nsf/termbox-go v1.1.1 // indirect +) @@ -0,0 +1,6 @@ +github.com/JoelOtter/termloop v0.0.0-20201118115657-7fa23b4da654 h1:pQt338SARhakABQ3KbycsnuKSg3XRDA5sF5RHl/v9WQ= +github.com/JoelOtter/termloop v0.0.0-20201118115657-7fa23b4da654/go.mod h1:Tie7OOEgasw91JpzA8UywemPyGehxZ06Gqtl5B1/vXI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= +github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= diff --git a/images/game_over_screen.png b/images/game_over_screen.png Binary files differnew file mode 100644 index 0000000..9fd2dd0 --- /dev/null +++ b/images/game_over_screen.png diff --git a/images/running_game_screen.png b/images/running_game_screen.png Binary files differnew file mode 100644 index 0000000..cabd95d --- /dev/null +++ b/images/running_game_screen.png diff --git a/images/title_screen.png b/images/title_screen.png Binary files differnew file mode 100644 index 0000000..1b28f0b --- /dev/null +++ b/images/title_screen.png diff --git a/invaders/alien.go b/invaders/alien.go new file mode 100644 index 0000000..f73b7e1 --- /dev/null +++ b/invaders/alien.go @@ -0,0 +1,92 @@ +package invaders + +import ( + _ "embed" + tl "github.com/JoelOtter/termloop" +) + +type Alien struct { + *tl.Entity + IsAlive bool + IsRendered bool + Points int +} + +type AlienType struct { + Source []byte + Points int +} + +var ( + //go:embed files/alien_basic.txt + alienBasicBytes []byte + + //go:embed files/alien_medium.txt + alienMediumBytes []byte + + //go:embed files/alien_strong.txt + alienStrongBytes []byte + + Basic = AlienType{Source: alienBasicBytes, Points: 10} + Medium = AlienType{Source: alienMediumBytes, Points: 20} + Strong = AlienType{Source: alienStrongBytes, Points: 30} +) + +func NewAlien(alienType AlienType) *Alien { + canvas := CreateCanvas(alienType.Source) + return &Alien{Entity: tl.NewEntityFromCanvas(0, 0, canvas), IsAlive: true, Points: alienType.Points} +} + +func CreateAliensLine(alienType AlienType, lineSize int) []*Alien { + aliens := make([]*Alien, lineSize) + for i := 0; i < lineSize; i++ { + aliens[i] = NewAlien(alienType) + } + + return aliens +} + +func SetPositionAndRenderAliens(aliens [][]*Alien, level *tl.BaseLevel, arena *Arena) { + initialX, initialY, space := calcInitialPositionAndSpace(aliens, arena) + + for index, line := range aliens { + x := initialX + + for _, alien := range line { + _, height := alien.Size() + y := initialY + height*(index+1) - 2 + + alien.SetPosition(x, y) + alien.IsRendered = true + + level.AddEntity(alien) + + x += space + } + } +} + +func calcInitialPositionAndSpace(aliens [][]*Alien, arena *Arena) (int, int, int) { + lineSize := len(aliens[0]) + alienW, _ := aliens[0][0].Size() + space := alienW + 1 + + arenaX, arenaY := arena.Position() + arenaW, _ := arena.Size() + + totalWidth := lineSize * space + x := arenaX + arenaW/2 - totalWidth/2 + + return x, arenaY, space +} + +func (alien *Alien) Collide(collision tl.Physical) { + if _, ok := collision.(*Laser); ok { + laser := collision.(*Laser) + + if laser.IsFromHero { + laser.HasHit = true + alien.IsAlive = false + } + } +} diff --git a/invaders/alien_cluster.go b/invaders/alien_cluster.go new file mode 100644 index 0000000..62bcdcf --- /dev/null +++ b/invaders/alien_cluster.go @@ -0,0 +1,214 @@ +package invaders + +import ( + tl "github.com/JoelOtter/termloop" + "math/rand" + "time" +) + +type AlienCluster struct { + Aliens [][]*Alien + Lasers []*Laser + TimeToMove float64 + WaitingTime float64 + WaitingTimeToMoveNextRow float64 + CurrentRowMoving int + Direction int + MoveSize int + IsMoving bool + IsMovingDown bool + IsAllDead bool + ReachedEndArena bool +} + +func NewAlienCluster() *AlienCluster { + lineSize := 13 + + alienCluster := AlienCluster{ + TimeToMove: 0, + WaitingTime: 1, + WaitingTimeToMoveNextRow: 0.07, + CurrentRowMoving: -1, + Direction: 3, + MoveSize: 3, + IsMoving: false, + ReachedEndArena: false, + IsMovingDown: false, + } + + alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Strong, lineSize)) + alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Medium, lineSize)) + alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Medium, lineSize)) + alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Basic, lineSize)) + alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Basic, lineSize)) + + return &alienCluster +} + +func (alienCluster *AlienCluster) UpdateAliensPositions(timeDelta float64, arena *Arena) { + alienCluster.prepareMovement() + + if alienCluster.canMove(timeDelta) { + alienCluster.move(arena) + } +} + +func (alienCluster *AlienCluster) prepareMovement() { + if alienCluster.CurrentRowMoving == -1 { + alienCluster.CurrentRowMoving = len(alienCluster.Aliens) - 1 + alienCluster.IsMoving = false + } + + if alienCluster.ReachedEndArena { + alienCluster.changeDirection() + } +} + +func (alienCluster *AlienCluster) changeDirection() { + alienCluster.Direction *= -1 + alienCluster.ReachedEndArena = false +} + +func (alienCluster *AlienCluster) canMove(timeDelta float64) bool { + alienCluster.TimeToMove += timeDelta + if alienCluster.isWaitingToMoveFirstRow() || alienCluster.isWaitingToMoveNextRow() { + return false + } + return true +} + +func (alienCluster *AlienCluster) isWaitingToMoveFirstRow() bool { + return alienCluster.IsMoving == false && alienCluster.TimeToMove < alienCluster.WaitingTime +} + +func (alienCluster *AlienCluster) isWaitingToMoveNextRow() bool { + return alienCluster.IsMoving && alienCluster.TimeToMove < alienCluster.WaitingTimeToMoveNextRow +} + +func (alienCluster *AlienCluster) move(arena *Arena) { + alienCluster.TimeToMove = 0 + alienCluster.IsMoving = true + + if alienCluster.IsMovingDown { + alienCluster.moveDown() + } else { + alienCluster.moveSideways(arena) + } + + alienCluster.CurrentRowMoving -= 1 +} + +func (alienCluster *AlienCluster) moveDown() { + row := alienCluster.getCurrentLine() + + for _, alien := range row { + x, y := alien.Position() + alien.SetPosition(x, y+alienCluster.MoveSize) + } + + finishedMovingRows := alienCluster.CurrentRowMoving == 0 + if finishedMovingRows { + alienCluster.IsMovingDown = false + } +} + +func (alienCluster *AlienCluster) moveSideways(arena *Arena) { + row := alienCluster.getCurrentLine() + + for _, alien := range row { + x, y := alien.Position() + w, _ := alien.Size() + x = x + alienCluster.Direction + + alien.SetPosition(x, y) + + if alien.IsAlive { + alienCluster.checkIfReachedEndOfArena(x, w, arena) + } + } +} + +func (alienCluster *AlienCluster) getCurrentLine() []*Alien { + row := alienCluster.Aliens[alienCluster.CurrentRowMoving] + return row +} + +func (alienCluster *AlienCluster) checkIfReachedEndOfArena(x int, w int, arena *Arena) { + if alienCluster.CurrentRowMoving == 0 && (x+w >= arena.End-alienCluster.MoveSize || x <= arena.Init+alienCluster.MoveSize) { + alienCluster.ReachedEndArena = true + alienCluster.IsMovingDown = true + } +} + +func (alienCluster *AlienCluster) RemoveDeadAliensAndGetPoints(level *tl.BaseLevel) int { + hasAtLeastOneAlive := false + points := 0 + + for _, alienRow := range alienCluster.Aliens { + for _, alien := range alienRow { + if alien.IsAlive == false && alien.IsRendered == true { + points += alien.Points + alien.IsRendered = false + + level.RemoveEntity(alien) + } + + if alien.IsAlive == true { + hasAtLeastOneAlive = true + } + } + } + + alienCluster.IsAllDead = !hasAtLeastOneAlive + + return points +} + +func (alienCluster *AlienCluster) Shoot() { + if alienCluster.canShoot() { + alien := alienCluster.selectRandomAlien() + + if alien == nil { + return + } + + x, y := alien.Position() + width, _ := alien.Size() + alienGunPosition := x + (width-1)/2 + distanceToAlien := y + 2 + + laser := NewAlienLaser(alienGunPosition, distanceToAlien) + alienCluster.Lasers = append(alienCluster.Lasers, laser) + } +} + +func (alienCluster *AlienCluster) canShoot() bool { + return len(alienCluster.Lasers) == 0 +} + +func (alienCluster *AlienCluster) selectRandomAlien() *Alien { + var shooterAlien *Alien + + rowSize := len(alienCluster.Aliens) - 1 + col := alienCluster.selectRandomAlienColumn() + + for row := rowSize; row >= 0; row-- { + alien := alienCluster.Aliens[row][col] + + if alien.IsAlive { + shooterAlien = alien + break + } + } + return shooterAlien +} + +func (alienCluster *AlienCluster) selectRandomAlienColumn() int { + rand.Seed(time.Now().UnixNano()) + col := rand.Intn(len(alienCluster.Aliens[0])) + return col +} + +func (alienCluster *AlienCluster) IsAllAliensDead() bool { + return alienCluster.IsAllDead +} diff --git a/invaders/arena.go b/invaders/arena.go new file mode 100644 index 0000000..440db57 --- /dev/null +++ b/invaders/arena.go @@ -0,0 +1,71 @@ +package invaders + +import ( + tl "github.com/JoelOtter/termloop" + "github.com/afagundes/go-invaders/invaders/utils" +) + +type Arena struct { + *tl.Entity + Init int + End int +} + +const ( + ArenaMaxWidth = 100 + ArenaMaxHeight = 37 +) + +func newArena(screenWidth int, screenHeight int) *Arena { + width := utils.ValueMinusPercent(screenWidth, 0.40) + height := utils.ValueMinusPercent(screenHeight, 0.15) + + if width > ArenaMaxWidth { + width = ArenaMaxWidth + } + + if height > ArenaMaxHeight { + height = ArenaMaxHeight + } + + centerX := screenWidth/2 - width/2 + centerY := screenHeight/2 - height/2 + init := centerX + 1 + end := centerX + width + + return &Arena{tl.NewEntityFromCanvas(centerX, centerY, createArena(width, height)), init, end} +} + +func createArena(width, height int) tl.Canvas { + canvas := tl.NewCanvas(width, height) + + for x, cell := range canvas { + for y := range cell { + fillTopBottom(x, y, height, canvas) + fillSides(x, y, width, canvas) + } + } + + createCell(0, 0, canvas, '┌') + createCell(width-1, 0, canvas, '┐') + createCell(0, height-1, canvas, '└') + createCell(width-1, height-1, canvas, '┘') + + return canvas +} + +func fillTopBottom(x, y, height int, canvas tl.Canvas) { + if x > 0 && (y == 0 || y == height-1) { + createCell(x, y, canvas, '─') + } +} + +func fillSides(x, y, width int, canvas tl.Canvas) { + if x == 0 || x == width-1 { + createCell(x, y, canvas, '│') + } +} + +func createCell(x, y int, canvas tl.Canvas, ch rune) { + canvas[x][y] = tl.Cell{Bg: tl.ColorBlack, Fg: tl.ColorWhite, Ch: ch} +} diff --git a/invaders/entity.go b/invaders/entity.go new file mode 100644 index 0000000..023aace --- /dev/null +++ b/invaders/entity.go @@ -0,0 +1,10 @@ +package invaders + +import ( + tl "github.com/JoelOtter/termloop" +) + +func CreateCanvas(fileContent []byte) tl.Canvas { + canvas := tl.CanvasFromString(string(fileContent)) + return canvas +} diff --git a/invaders/files/alien_basic.txt b/invaders/files/alien_basic.txt new file mode 100644 index 0000000..ca67a36 --- /dev/null +++ b/invaders/files/alien_basic.txt @@ -0,0 +1,2 @@ +▄██▄ + diff --git a/invaders/files/alien_medium.txt b/invaders/files/alien_medium.txt new file mode 100644 index 0000000..1e19062 --- /dev/null +++ b/invaders/files/alien_medium.txt @@ -0,0 +1,2 @@ +▄██▄ +▀ ▀ diff --git a/invaders/files/alien_strong.txt b/invaders/files/alien_strong.txt new file mode 100644 index 0000000..25853d0 --- /dev/null +++ b/invaders/files/alien_strong.txt @@ -0,0 +1,2 @@ +█ █ +▀██▀ diff --git a/invaders/files/game_over.txt b/invaders/files/game_over.txt new file mode 100644 index 0000000..971d929 --- /dev/null +++ b/invaders/files/game_over.txt @@ -0,0 +1,6 @@ + ██████╗ █████╗ ███╗ ███╗███████╗ ██████╗ ██╗ ██╗███████╗██████╗ +██╔════╝ ██╔══██╗████╗ ████║██╔════╝ ██╔═══██╗██║ ██║██╔════╝██╔══██╗ +██║ ███╗███████║██╔████╔██║█████╗ ██║ ██║██║ ██║█████╗ ██████╔╝ +██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ +╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ ╚██████╔╝ ╚████╔╝ ███████╗██║ ██║ + ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ diff --git a/invaders/files/hero.txt b/invaders/files/hero.txt new file mode 100644 index 0000000..1edcd05 --- /dev/null +++ b/invaders/files/hero.txt @@ -0,0 +1,2 @@ + ▄ +███ diff --git a/invaders/files/title.txt b/invaders/files/title.txt new file mode 100644 index 0000000..fba236e --- /dev/null +++ b/invaders/files/title.txt @@ -0,0 +1,6 @@ + ██████╗ ██████╗ ██╗███╗ ██╗██╗ ██╗ █████╗ ██████╗ ███████╗██████╗ ███████╗ +██╔════╝ ██╔═══██╗ ██║████╗ ██║██║ ██║██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝ +██║ ███╗██║ ██║ ██║██╔██╗ ██║██║ ██║███████║██║ ██║█████╗ ██████╔╝███████╗ +██║ ██║██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══██║██║ ██║██╔══╝ ██╔══██╗╚════██║ +╚██████╔╝╚██████╔╝ ██║██║ ╚████║ ╚████╔╝ ██║ ██║██████╔╝███████╗██║ ██║███████║ + ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═══╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚══════╝ diff --git a/invaders/game.go b/invaders/game.go new file mode 100644 index 0000000..54052de --- /dev/null +++ b/invaders/game.go @@ -0,0 +1,217 @@ +package invaders + +import ( + tl "github.com/JoelOtter/termloop" + "time" +) + +type Invaders struct { + *tl.Entity + Game *tl.Game + Level *tl.BaseLevel + Arena *Arena + GameOverZone *GameOverZone + Hud *Hud + Hero *Hero + AlienCluster *AlienCluster + AlienLaserVelocity float64 + TimeDelta float64 + RefreshSpeed time.Duration + Score int + Started bool + ScreenSizeNotOK bool +} + +func NewGame() *Invaders { + invaders := Invaders{ + Entity: tl.NewEntity(0, 0, 1, 1), + Game: tl.NewGame(), + Level: tl.NewBaseLevel(tl.Cell{Bg: tl.ColorBlack, Fg: tl.ColorWhite}), + AlienLaserVelocity: 0.04, + RefreshSpeed: 20, + Score: 0, + } + + invaders.Game.Screen().SetFps(60) + invaders.Game.SetEndKey(tl.KeyBackspace) + invaders.Game.Screen().SetLevel(invaders.Level) + invaders.Level.AddEntity(&invaders) + + return &invaders +} + +func (invaders *Invaders) Start() { + go ShowTitleScreen(invaders) + invaders.Game.Start() +} + +func (invaders *Invaders) Tick(event tl.Event) { + if invaders.Started == false && invaders.ScreenSizeNotOK == false && event.Type == tl.EventKey && event.Key == tl.KeyEnter { + go invaders.initializeGame() + } +} + +func (invaders *Invaders) initializeGame() { + prepareScreen(invaders) + + invaders.Started = true + + invaders.initHero() + invaders.initAliens() + invaders.initGameOverZone() + invaders.gameLoop() +} + +func (invaders *Invaders) initArena() { + screenWidth, screenHeight := invaders.getScreenSize() + invaders.Arena = newArena(screenWidth, screenHeight) + invaders.Level.AddEntity(invaders.Arena) +} + +func (invaders *Invaders) initHud() { + invaders.Hud = NewHud(invaders.Arena, invaders.Level) +} + +func (invaders *Invaders) getScreenSize() (int, int) { + screenWidth, screenHeight := invaders.Game.Screen().Size() + + for screenWidth == 0 && screenHeight == 0 { + time.Sleep(100 * time.Millisecond) + screenWidth, screenHeight = invaders.Game.Screen().Size() + } + + return screenWidth, screenHeight +} + +func (invaders *Invaders) initHero() { + invaders.Hero = NewHero(invaders.Arena) + invaders.Level.AddEntity(invaders.Hero) +} + +func (invaders *Invaders) initAliens() { + invaders.AlienCluster = NewAlienCluster() + SetPositionAndRenderAliens(invaders.AlienCluster.Aliens, invaders.Level, invaders.Arena) +} + +func (invaders *Invaders) initGameOverZone() { + invaders.GameOverZone = CreateGameOverZone(invaders.Arena, invaders.Hero) + invaders.Level.AddEntity(invaders.GameOverZone) +} + +func (invaders *Invaders) gameLoop() { + for { + if invaders.Hero.IsDead() || invaders.AlienCluster.IsAllAliensDead() { + invaders.Hero.animateHeroEndGame(invaders.Level) + invaders.Started = false + break + } + + invaders.updateLaserPositions() + invaders.RemoveDeadAliensAndIncrementScore() + invaders.updateAlienClusterPosition() + invaders.updateScore() + invaders.verifyGameOverZone() + + time.Sleep(invaders.RefreshSpeed * time.Millisecond) + } + + if invaders.Hero.IsDead() { + ShowGameOverScreen(invaders) + } + + if invaders.AlienCluster.IsAllAliensDead() { + invaders.initializeGame() + } +} + +func (invaders *Invaders) updateScore() { + invaders.Hud.UpdateScore(invaders.Score) +} + +func (invaders *Invaders) updateAlienClusterPosition() { + invaders.AlienCluster.UpdateAliensPositions(invaders.Game.Screen().TimeDelta(), invaders.Arena) + invaders.AlienCluster.Shoot() +} + +func (invaders *Invaders) RemoveDeadAliensAndIncrementScore() { + points := invaders.AlienCluster.RemoveDeadAliensAndGetPoints(invaders.Level) + invaders.addScore(points) +} + +func (invaders *Invaders) updateLaserPositions() { + invaders.updateHeroLasers() + invaders.updateAlienLasers() + invaders.removeLasers() +} + +func (invaders *Invaders) updateHeroLasers() { + invaders.updateLasers(invaders.Hero.Lasers) +} + +func (invaders *Invaders) updateAlienLasers() { + invaders.TimeDelta += invaders.Game.Screen().TimeDelta() + + if invaders.TimeDelta >= invaders.AlienLaserVelocity { + invaders.TimeDelta = 0 + invaders.updateLasers(invaders.AlienCluster.Lasers) + } +} + +func (invaders *Invaders) updateLasers(lasers []*Laser) { + for _, laser := range lasers { + if laser.IsNew { + invaders.renderNewLaser(laser) + continue + } + + x, y := laser.Position() + laser.SetPosition(x, y-laser.Direction) + } +} + +func (invaders *Invaders) renderNewLaser(laser *Laser) { + laser.IsNew = false + invaders.Level.AddEntity(laser) +} + +func (invaders *Invaders) removeLasers() { + _, arenaY := invaders.Arena.Position() + _, arenaH := invaders.Arena.Size() + + upperLimit := arenaY + bottomLimit := arenaY + arenaH - 1 + + invaders.Hero.Lasers = invaders.removeLaserOf(invaders.Hero.Lasers, upperLimit) + invaders.AlienCluster.Lasers = invaders.removeLaserOf(invaders.AlienCluster.Lasers, bottomLimit) +} + +func (invaders *Invaders) removeLaserOf(lasers []*Laser, arenaLimit int) []*Laser { + for index, laser := range lasers { + _, y := laser.Position() + isEndOfArena := y == arenaLimit + + if isEndOfArena || laser.HasHit { + invaders.Level.RemoveEntity(laser) + + if laser.HitAlienLaser { + invaders.addScore(laser.Points) + } + + if index < len(lasers) { + lasers = append(lasers[:index], lasers[index+1:]...) + } + } + } + + return lasers +} + +func (invaders *Invaders) addScore(points int) { + invaders.Score += points +} + +func (invaders *Invaders) verifyGameOverZone() { + if invaders.GameOverZone.EnteredZone { + invaders.Hero.IsAlive = false + } +} diff --git a/invaders/game_over_zone.go b/invaders/game_over_zone.go new file mode 100644 index 0000000..702deb4 --- /dev/null +++ b/invaders/game_over_zone.go @@ -0,0 +1,27 @@ +package invaders + +import tl "github.com/JoelOtter/termloop" + +type GameOverZone struct { + *tl.Entity + EnteredZone bool +} + +func CreateGameOverZone(arena *Arena, hero *Hero) *GameOverZone { + _, heroH := hero.Size() + arenaX, arenaY := arena.Position() + arenaW, arenaH := arena.Size() + + x := arenaX + y := arenaY + arenaH - heroH + w := arenaW + h := heroH + + return &GameOverZone{Entity: tl.NewEntity(x, y, w, h)} +} + +func (gameOverZone *GameOverZone) Collide(collision tl.Physical) { + if _, ok := collision.(*Alien); ok && collision.(*Alien).IsAlive { + gameOverZone.EnteredZone = true + } +} diff --git a/invaders/hero.go b/invaders/hero.go new file mode 100644 index 0000000..c9b4500 --- /dev/null +++ b/invaders/hero.go @@ -0,0 +1,104 @@ +package invaders + +import ( + _ "embed" + tl "github.com/JoelOtter/termloop" + "time" +) + +type Hero struct { + *tl.Entity + Arena *Arena + Lasers []*Laser + IsAlive bool +} + +//go:embed files/hero.txt +var heroBytes []byte + +func NewHero(arena *Arena) *Hero { + heroCanvas := CreateCanvas(heroBytes) + x, y := setHeroPosition(arena, heroCanvas) + + return &Hero{Entity: tl.NewEntityFromCanvas(x, y, heroCanvas), Arena: arena, IsAlive: true} +} + +func setHeroPosition(arena *Arena, heroCanvas tl.Canvas) (int, int) { + arenaX, arenaY := arena.Position() + arenaW, arenaH := arena.Size() + + x := arenaX + arenaW/2 - len(heroCanvas)/2 + y := arenaY + arenaH - len(heroCanvas[0]) + + return x, y +} + +func (hero *Hero) Tick(event tl.Event) { + if hero.IsAlive == false { + return + } + + if event.Type == tl.EventKey { + x, y := hero.Position() + heroWidth, _ := hero.Size() + + switch event.Key { + case tl.KeyArrowLeft: + if x > hero.Arena.Init { + x = x - 1 + hero.SetPosition(x, y) + } + case tl.KeyArrowRight: + if x < hero.Arena.End-heroWidth-1 { + x = x + 1 + hero.SetPosition(x, y) + } + case tl.KeySpace: + hero.shoot() + } + } +} + +func (hero *Hero) shoot() { + x, y := hero.Position() + + if hero.isReloading() { + return + } + + heroWidth, _ := hero.Size() + heroGunPosition := x + (heroWidth-1)/2 + distanceToHero := y - 1 + + laser := NewHeroLaser(heroGunPosition, distanceToHero) + hero.Lasers = append(hero.Lasers, laser) +} + +func (hero *Hero) isReloading() bool { + return len(hero.Lasers) > 0 +} + +func (hero *Hero) Collide(collision tl.Physical) { + if _, ok := collision.(*Laser); ok { + laser := collision.(*Laser) + + laser.HasHit = true + hero.IsAlive = false + } +} + +func (hero *Hero) IsDead() bool { + return hero.IsAlive == false +} + +func (hero *Hero) animateHeroEndGame(level *tl.BaseLevel) { + for i := 0; i < 6; i++ { + if i%2 == 0 { + level.RemoveEntity(hero) + } else { + level.AddEntity(hero) + } + + time.Sleep(450 * time.Millisecond) + } +} diff --git a/invaders/hud.go b/invaders/hud.go new file mode 100644 index 0000000..cbe2c8e --- /dev/null +++ b/invaders/hud.go @@ -0,0 +1,61 @@ +package invaders + +import ( + "fmt" + tl "github.com/JoelOtter/termloop" +) + +type Hud struct { + Title string + PressToQuit string + Score *tl.FpsText +} + +func NewHud(arena *Arena, level *tl.BaseLevel) *Hud { + hud := Hud{Title: "Go Invaders", PressToQuit: "Press BACKSPACE to quit"} + hud.drawTitle(arena, level) + hud.drawPressToQuit(arena, level) + hud.drawScore(arena, level) + + return &hud +} + +func (hud *Hud) drawTitle(arena *Arena, level *tl.BaseLevel) { + arenaX, arenaY := arena.Position() + x := arenaX + 1 + y := arenaY - 1 + + title := tl.NewText(x, y, hud.Title, tl.ColorWhite, tl.ColorBlack) + level.AddEntity(title) +} + +func (hud *Hud) drawPressToQuit(arena *Arena, level *tl.BaseLevel) { + arenaX, arenaY := arena.Position() + arenaW, arenaH := arena.Size() + + x := arenaX + arenaW - len(hud.PressToQuit) - 1 + y := arenaY + arenaH + + title := tl.NewText(x, y, hud.PressToQuit, tl.ColorWhite, tl.ColorBlack) + level.AddEntity(title) +} + +func (hud *Hud) drawScore(arena *Arena, level *tl.BaseLevel) { + arenaX, arenaY := arena.Position() + _, arenaH := arena.Size() + + x := arenaX + 1 + y := arenaY + arenaH + + hud.Score = tl.NewFpsText(x, y, tl.ColorWhite, tl.ColorBlack, 60) + level.AddEntity(hud.Score) +} + +func (hud *Hud) UpdateScore(score int) { + hud.Score.SetText(hud.getScoreText(score)) +} + +func (hud *Hud) getScoreText(score int) string { + txtScore := fmt.Sprintf("Score: %4d", score) + return txtScore +} diff --git a/invaders/laser.go b/invaders/laser.go new file mode 100644 index 0000000..d2d532d --- /dev/null +++ b/invaders/laser.go @@ -0,0 +1,46 @@ +package invaders + +import tl "github.com/JoelOtter/termloop" + +type Laser struct { + *tl.Rectangle + Direction int + IsFromHero bool + IsNew bool + HasHit bool + HitAlienLaser bool + Points int +} + +func NewHeroLaser(heroGunPosition int, y int) *Laser { + return &Laser{ + Rectangle: tl.NewRectangle(heroGunPosition, y, 1, 1, tl.ColorRed), + Direction: 1, + IsNew: true, + IsFromHero: true, + } +} + +func NewAlienLaser(alienGunPosition int, y int) *Laser { + return &Laser{ + Rectangle: tl.NewRectangle(alienGunPosition, y, 1, 1, tl.ColorGreen), + Direction: -1, + IsNew: true, + IsFromHero: false, + Points: 5, + } +} + +func (laser *Laser) Collide(collision tl.Physical) { + if laser.IsFromHero == false { + return + } + + if laserCollide, isLaser := collision.(*Laser); isLaser { + laser.HasHit = true + laser.HitAlienLaser = true + laser.Points = laserCollide.Points + + laserCollide.HasHit = true + } +} diff --git a/invaders/screens.go b/invaders/screens.go new file mode 100644 index 0000000..739ce70 --- /dev/null +++ b/invaders/screens.go @@ -0,0 +1,97 @@ +package invaders + +import ( + _ "embed" + "fmt" + tl "github.com/JoelOtter/termloop" +) + +var ( + //go:embed files/title.txt + titleScreenFile []byte + + //go:embed files/game_over.txt + gameOverScreenFile []byte +) + +func ShowTitleScreen(invaders *Invaders) { + prepareScreen(invaders) + showTitle(invaders) + + if checkArenaSizeNotOk(invaders) { + invaders.ScreenSizeNotOK = true + showMaximizeScreen(invaders) + return + } + + showPressToInit(invaders, 0) +} + +func checkArenaSizeNotOk(invaders *Invaders) bool { + w, h := invaders.Arena.Size() + + if w < 100 || h < 37 { + return true + } + + return false +} + +func ShowGameOverScreen(invaders *Invaders) { + prepareScreen(invaders) + showGameOver(invaders) + showScore(invaders) + showPressToInit(invaders, 2) +} + +func prepareScreen(invaders *Invaders) { + invaders.Level = tl.NewBaseLevel(tl.Cell{Bg: tl.ColorBlack, Fg: tl.ColorWhite}) + invaders.Game.Screen().SetLevel(invaders.Level) + invaders.Level.AddEntity(invaders) + + invaders.initArena() + invaders.initHud() +} + +func showTitle(invaders *Invaders) { + showCanvas(invaders, titleScreenFile) +} + +func showGameOver(invaders *Invaders) { + showCanvas(invaders, gameOverScreenFile) +} + +func showCanvas(invaders *Invaders, file []byte) { + canvas := CreateCanvas(file) + + arenaX, arenaY := invaders.Arena.Position() + arenaW, arenaH := invaders.Arena.Size() + + x := arenaX + arenaW/2 - len(canvas)/2 + y := arenaY + arenaH/2 + -len(canvas[0]) - 1 + + invaders.Level.AddEntity(tl.NewEntityFromCanvas(x, y, canvas)) +} + +func showScore(invaders *Invaders) { + score := fmt.Sprintf("SCORE: %4d ", invaders.Score) + showCenterText(score, 0, invaders) +} + +func showPressToInit(invaders *Invaders, topPadding int) { + showCenterText("Press ENTER to start", topPadding, invaders) +} + +func showMaximizeScreen(invaders *Invaders) { + showCenterText("Maximize the console and run the game again", 0, invaders) +} + +func showCenterText(text string, topPadding int, invaders *Invaders) { + arenaX, arenaY := invaders.Arena.Position() + arenaW, arenaH := invaders.Arena.Size() + + x := arenaX + arenaW/2 - len(text)/2 + y := arenaY + arenaH/2 + topPadding + + invaders.Level.AddEntity(tl.NewText(x, y, text, tl.ColorWhite, tl.ColorBlack)) +} diff --git a/invaders/utils/utils.go b/invaders/utils/utils.go new file mode 100644 index 0000000..cd13004 --- /dev/null +++ b/invaders/utils/utils.go @@ -0,0 +1,5 @@ +package utils + +func ValueMinusPercent(val int, percentage float64) int { + return int(float64(val) - float64(val)*percentage) +} @@ -0,0 +1,7 @@ +package main + +import "github.com/afagundes/go-invaders/invaders" + +func main() { + invaders.NewGame().Start() +} |
