├── CREDITS ├── .gitignore ├── screenshot.png ├── .gitmodules ├── assets └── metadata │ ├── fonts │ └── fonts.toml │ ├── entities │ ├── background.toml │ ├── player_line.toml │ ├── player.toml │ ├── ui │ │ ├── life.toml │ │ ├── score.toml │ │ ├── difficulty.toml │ │ ├── mute_menu.toml │ │ ├── level_complete_menu.toml │ │ ├── difficulty_menu.toml │ │ ├── pause_menu.toml │ │ ├── main_menu.toml │ │ ├── game_over_menu.toml │ │ └── highscores_menu.toml │ ├── player_bullet.toml │ ├── enemy_bullet.toml │ ├── alien_master.toml │ ├── bunker.toml │ └── alien.toml │ └── spritesheets │ └── spritesheets.toml ├── lib ├── resources │ ├── controls.go │ ├── prefab.go │ ├── animations.go │ └── game.go ├── math │ └── lib.go ├── systems │ ├── sound.go │ ├── delete.go │ ├── move_bullet.go │ ├── score.go │ ├── move_alien_master.go │ ├── move_player.go │ ├── life.go │ ├── spawn_alien_master.go │ ├── shoot_player_bullet.go │ ├── shoot_enemy_bullet.go │ ├── move_alien.go │ └── collision.go ├── components │ └── lib.go ├── loader │ ├── sound.go │ ├── lib.go │ └── bunker.go └── states │ ├── mute_menu.go │ ├── game_over_menu.go │ ├── pause_menu.go │ ├── level_complete_menu.go │ ├── menu.go │ ├── main_menu.go │ ├── difficulty_menu.go │ ├── death.go │ ├── gameplay.go │ └── highscores_menu.go ├── README.md ├── LICENSE ├── go.mod ├── config └── controls.toml ├── main.go └── go.sum /CREDITS: -------------------------------------------------------------------------------- 1 | Music (Wave After Wave!): FoxSynergy 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | config/highscores.toml 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-hgg-x/space-invaders-go/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets/lfs"] 2 | path = assets/lfs 3 | url = ./ 4 | branch = lfs 5 | -------------------------------------------------------------------------------- /assets/metadata/fonts/fonts.toml: -------------------------------------------------------------------------------- 1 | [font.joystix] 2 | font = "assets/lfs/fonts/joystix.ttf" 3 | 4 | [font.hack] 5 | font = "assets/lfs/fonts/hack.ttf" 6 | -------------------------------------------------------------------------------- /assets/metadata/entities/background.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | sprite_sheet_name = "background" 5 | sprite_number = 0 6 | 7 | [entity.components.Transform] 8 | translation = { x = 0.0, y = 0.0 } 9 | origin = "Middle" 10 | depth = -1.0 11 | -------------------------------------------------------------------------------- /assets/metadata/entities/player_line.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | fill = { width = 1000, height = 1, color = [255, 255, 255, 255] } 5 | 6 | [entity.components.Transform] 7 | translation = { x = 0.0, y = 57.0 } 8 | origin = "BottomMiddle" 9 | 10 | [entity.components.PlayerLine] 11 | -------------------------------------------------------------------------------- /assets/metadata/entities/player.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | sprite_sheet_name = "game" 5 | sprite_number = 16 6 | 7 | [entity.components.Transform] 8 | translation = { x = 500.0, y = 30.0 } 9 | 10 | [entity.components.Player] 11 | 12 | [entity.components.Controllable] 13 | width = 90.0 14 | height = 48.0 15 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/life.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.Text] 4 | id = "game_life" 5 | text = "LIVES: 3" 6 | font_face = { font = "joystix", options.size = 25.0 } 7 | color = [255, 255, 255, 255] 8 | 9 | [entity.components.UITransform] 10 | translation = { x = -10, y = -10 } 11 | origin = "TopRight" 12 | pivot = "TopRight" 13 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/score.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.Text] 4 | id = "game_score" 5 | text = "SCORE: 0" 6 | font_face = { font = "joystix", options.size = 25.0 } 7 | color = [255, 255, 255, 255] 8 | 9 | [entity.components.UITransform] 10 | translation = { x = 10, y = -10 } 11 | origin = "TopLeft" 12 | pivot = "TopLeft" 13 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/difficulty.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.Text] 4 | id = "game_difficulty" 5 | text = "NORMAL" 6 | font_face = { font = "joystix", options.size = 25.0 } 7 | color = [255, 255, 255, 255] 8 | 9 | [entity.components.UITransform] 10 | translation = { x = 0, y = -10 } 11 | origin = "TopMiddle" 12 | pivot = "TopMiddle" 13 | -------------------------------------------------------------------------------- /lib/resources/controls.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | const ( 4 | // PlayerAxis is the axis for moving player 5 | PlayerAxis = "Player" 6 | // ShootAction is the action for shooting bullets 7 | ShootAction = "Shoot" 8 | // EnableDisableSoundAction is the action for enabling and disabling sound 9 | EnableDisableSoundAction = "EnableDisableSound" 10 | ) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space-invaders-go 2 | 3 | Space Invaders game in Go using Ebitengine with ECS. 4 | 5 | You must use the `--recursive` or `--recurse-submodules` flag when cloning the repository to initialize submodules. 6 | 7 | See [config/controls.toml](config/controls.toml) for editing controls. 8 | 9 | ## Screenshot 10 | 11 | ![screenshot](screenshot.png) 12 | -------------------------------------------------------------------------------- /lib/math/lib.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | // Abs returns the absolute value of an integer 4 | func Abs(x int) int { 5 | if x < 0 { 6 | return -x 7 | } 8 | return x 9 | } 10 | 11 | // Mod returns the Euclidean division remainder between 2 integers 12 | func Mod(a, b int) int { 13 | m := a % b 14 | if m < 0 { 15 | m += Abs(b) 16 | } 17 | return m 18 | } 19 | -------------------------------------------------------------------------------- /assets/metadata/entities/player_bullet.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | sprite_sheet_name = "game" 5 | sprite_number = 11 6 | 7 | [entity.components.Transform] 8 | translation.y = 82.0 9 | depth = 0.5 10 | 11 | [entity.components.Player] 12 | 13 | [entity.components.Bullet] 14 | width = 6.0 15 | height = 48.0 16 | velocity = 450.0 17 | health = 250.0 18 | -------------------------------------------------------------------------------- /assets/metadata/entities/enemy_bullet.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | sprite_sheet_name = "game" 5 | sprite_number = 9 6 | 7 | [entity.components.Transform] 8 | depth = 0.4 9 | 10 | [entity.components.AnimationControl] 11 | sprite_sheet_name = "game" 12 | animation_name = "enemy_bullet" 13 | end.type = "Loop" 14 | 15 | [entity.components.Enemy] 16 | 17 | [entity.components.Bullet] 18 | width = 12.0 19 | height = 48.0 20 | velocity = -200.0 21 | health = 500.0 22 | -------------------------------------------------------------------------------- /assets/metadata/entities/alien_master.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | sprite_sheet_name = "game" 5 | sprite_number = 13 6 | 7 | [entity.components.Transform] 8 | translation.y = 760.0 9 | 10 | [entity.components.AnimationControl] 11 | sprite_sheet_name = "game" 12 | animation_name = "alien_master_loop" 13 | end.type = "Loop" 14 | 15 | [entity.components.Enemy] 16 | 17 | [entity.components.Alien] 18 | width = 96.0 19 | height = 42.0 20 | 21 | [entity.components.AlienMaster] 22 | -------------------------------------------------------------------------------- /lib/systems/sound.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "github.com/x-hgg-x/space-invaders-go/lib/loader" 5 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 6 | 7 | w "github.com/x-hgg-x/goecsengine/world" 8 | ) 9 | 10 | // SoundSystem manages sound 11 | func SoundSystem(world w.World) { 12 | if world.Resources.InputHandler.Actions[resources.EnableDisableSoundAction] { 13 | audioPlayers := *world.Resources.AudioPlayers 14 | if audioPlayers["music"].Volume() == 0 { 15 | loader.EnableSound(world) 16 | } else { 17 | loader.DisableSound(world) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/systems/delete.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 5 | 6 | ecs "github.com/x-hgg-x/goecs/v2" 7 | ec "github.com/x-hgg-x/goecsengine/components" 8 | w "github.com/x-hgg-x/goecsengine/world" 9 | ) 10 | 11 | // DeleteSystem removes deleted entities after animation end 12 | func DeleteSystem(world w.World) { 13 | gameComponents := world.Components.Game.(*gc.Components) 14 | 15 | world.Manager.Join(gameComponents.Deleted, world.Components.Engine.AnimationControl).Visit(ecs.Visit(func(entity ecs.Entity) { 16 | animationControl := world.Components.Engine.AnimationControl.Get(entity).(*ec.AnimationControl) 17 | if animationControl.GetState().Type == ec.ControlStateDone { 18 | world.Manager.DeleteEntity(entity) 19 | } 20 | })) 21 | } 22 | -------------------------------------------------------------------------------- /lib/systems/move_bullet.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 5 | 6 | ecs "github.com/x-hgg-x/goecs/v2" 7 | ec "github.com/x-hgg-x/goecsengine/components" 8 | w "github.com/x-hgg-x/goecsengine/world" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | ) 12 | 13 | // MoveBulletSystem moves bullet 14 | func MoveBulletSystem(world w.World) { 15 | gameComponents := world.Components.Game.(*gc.Components) 16 | 17 | world.Manager.Join(gameComponents.Bullet, world.Components.Engine.Transform).Visit(ecs.Visit(func(entity ecs.Entity) { 18 | bulletVelocity := gameComponents.Bullet.Get(entity).(*gc.Bullet).Velocity 19 | bulletTransform := world.Components.Engine.Transform.Get(entity).(*ec.Transform) 20 | bulletTransform.Translation.Y += bulletVelocity / ebiten.DefaultTPS 21 | })) 22 | } 23 | -------------------------------------------------------------------------------- /assets/metadata/entities/bunker.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | sprite_sheet_name = "bunker" 5 | sprite_number = 0 6 | 7 | [entity.components.Transform] 8 | translation = { x = 200.0, y = 126.0 } 9 | depth = -0.1 10 | 11 | [entity.components.Bunker] 12 | pixel_size = 12 13 | 14 | 15 | [[entity]] 16 | 17 | [entity.components.SpriteRender] 18 | sprite_sheet_name = "bunker" 19 | sprite_number = 0 20 | 21 | [entity.components.Transform] 22 | translation = { x = 500.0, y = 126.0 } 23 | depth = -0.1 24 | 25 | [entity.components.Bunker] 26 | pixel_size = 12 27 | 28 | 29 | [[entity]] 30 | 31 | [entity.components.SpriteRender] 32 | sprite_sheet_name = "bunker" 33 | sprite_number = 0 34 | 35 | [entity.components.Transform] 36 | translation = { x = 800.0, y = 126.0 } 37 | depth = -0.1 38 | 39 | [entity.components.Bunker] 40 | pixel_size = 12 41 | -------------------------------------------------------------------------------- /lib/systems/score.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 7 | 8 | ecs "github.com/x-hgg-x/goecs/v2" 9 | ec "github.com/x-hgg-x/goecsengine/components" 10 | "github.com/x-hgg-x/goecsengine/math" 11 | w "github.com/x-hgg-x/goecsengine/world" 12 | ) 13 | 14 | // ScoreSystem manages score 15 | func ScoreSystem(world w.World) { 16 | gameResources := world.Resources.Game.(*resources.Game) 17 | 18 | for _, scoreEvent := range gameResources.Events.ScoreEvents { 19 | gameResources.Score += scoreEvent.Score 20 | gameResources.Score = math.Min(99999, gameResources.Score) 21 | 22 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 23 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 24 | if text.ID == "game_score" { 25 | text.Text = fmt.Sprintf("SCORE: %d", gameResources.Score) 26 | } 27 | })) 28 | } 29 | gameResources.Events.ScoreEvents = nil 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 x-hgg-x 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/x-hgg-x/space-invaders-go 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.2.1 7 | github.com/hajimehoshi/ebiten/v2 v2.4.12 8 | github.com/x-hgg-x/goecs/v2 v2.0.5 9 | github.com/x-hgg-x/goecsengine v0.11.2 10 | ) 11 | 12 | require ( 13 | github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 // indirect 14 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad // indirect 15 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 16 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 // indirect 17 | github.com/hajimehoshi/go-mp3 v0.3.3 // indirect 18 | github.com/hajimehoshi/oto/v2 v2.3.1 // indirect 19 | github.com/jezek/xgb v1.0.1 // indirect 20 | github.com/jfreymuth/oggvorbis v1.0.4 // indirect 21 | github.com/jfreymuth/vorbis v1.0.2 // indirect 22 | github.com/yourbasic/bit v0.0.0-20180313074424-45a4409f4082 // indirect 23 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 24 | golang.org/x/image v0.18.0 // indirect 25 | golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect 26 | golang.org/x/sys v0.5.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /lib/systems/move_alien_master.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 5 | 6 | ecs "github.com/x-hgg-x/goecs/v2" 7 | ec "github.com/x-hgg-x/goecsengine/components" 8 | w "github.com/x-hgg-x/goecsengine/world" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | ) 12 | 13 | // MoveAlienMasterSystem moves alien master 14 | func MoveAlienMasterSystem(world w.World) { 15 | gameComponents := world.Components.Game.(*gc.Components) 16 | 17 | world.Manager.Join(gameComponents.Alien, gameComponents.AlienMaster, world.Components.Engine.Transform).Visit(ecs.Visit(func(entity ecs.Entity) { 18 | alien := gameComponents.Alien.Get(entity).(*gc.Alien) 19 | alienMaster := gameComponents.AlienMaster.Get(entity).(*gc.AlienMaster) 20 | alienMasterTranslation := &world.Components.Engine.Transform.Get(entity).(*ec.Transform).Translation 21 | alienMasterTranslation.X += alienMaster.Direction * float64(world.Resources.ScreenDimensions.Width) / 4 / ebiten.DefaultTPS 22 | 23 | if alienMasterTranslation.X <= -alien.Width/2 || alienMasterTranslation.X >= float64(world.Resources.ScreenDimensions.Width)+alien.Width/2 { 24 | world.Manager.DeleteEntity(entity) 25 | } 26 | })) 27 | } 28 | -------------------------------------------------------------------------------- /lib/resources/prefab.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import "github.com/x-hgg-x/goecsengine/loader" 4 | 5 | // MenuPrefabs contains menu prefabs 6 | type MenuPrefabs struct { 7 | MuteMenu loader.EntityComponentList 8 | MainMenu loader.EntityComponentList 9 | DifficultyMenu loader.EntityComponentList 10 | PauseMenu loader.EntityComponentList 11 | GameOverMenu loader.EntityComponentList 12 | LevelCompleteMenu loader.EntityComponentList 13 | HighscoresMenu loader.EntityComponentList 14 | } 15 | 16 | // GamePrefabs contains game prefabs 17 | type GamePrefabs struct { 18 | Background loader.EntityComponentList 19 | Alien loader.EntityComponentList 20 | Player loader.EntityComponentList 21 | PlayerLine loader.EntityComponentList 22 | Bunker loader.EntityComponentList 23 | AlienMaster loader.EntityComponentList 24 | PlayerBullet loader.EntityComponentList 25 | EnemyBullet loader.EntityComponentList 26 | Score loader.EntityComponentList 27 | Life loader.EntityComponentList 28 | Difficulty loader.EntityComponentList 29 | } 30 | 31 | // Prefabs contains menu and game prefabs 32 | type Prefabs struct { 33 | Menu MenuPrefabs 34 | Game GamePrefabs 35 | } 36 | -------------------------------------------------------------------------------- /lib/resources/animations.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | const ( 4 | // AlienLoop1Animation is the animation for alien 1 5 | AlienLoop1Animation = "alien_loop_1" 6 | // AlienLoop2Animation is the animation for alien 2 7 | AlienLoop2Animation = "alien_loop_2" 8 | // AlienLoop3Animation is the animation for alien 3 9 | AlienLoop3Animation = "alien_loop_3" 10 | // AlienDeath1Animation is the animation for the death of alien 1 11 | AlienDeath1Animation = "alien_death_1" 12 | // AlienDeath2Animation is the animation for the death of alien 2 13 | AlienDeath2Animation = "alien_death_2" 14 | // AlienDeath3Animation is the animation for the death of alien 3 15 | AlienDeath3Animation = "alien_death_3" 16 | // EnemyBulletAnimation is the animation for enemy bullet 17 | EnemyBulletAnimation = "enemy_bullet" 18 | // PlayerBulletExplosionAnimation is the animation for player bullet explosion 19 | PlayerBulletExplosionAnimation = "player_bullet_explosion" 20 | // AlienMasterLoopAnimation is the animation for alien master 21 | AlienMasterLoopAnimation = "alien_master_loop" 22 | // AlienMasterDeathAnimation is the animation for the death of alien master 23 | AlienMasterDeathAnimation = "alien_master_death" 24 | // PlayerDeathAnimation is the animation for the death of player 25 | PlayerDeathAnimation = "player_death" 26 | ) 27 | -------------------------------------------------------------------------------- /config/controls.toml: -------------------------------------------------------------------------------- 1 | [controls.axes.Player] 2 | mouse_axis.axis = 0 3 | 4 | # [controls.axes.Player] 5 | # emulated.neg.key = "Left" 6 | # emulated.pos.key = "Right" 7 | 8 | [controls.actions.Shoot] 9 | combinations = [[{ mouse_button = "MouseButtonLeft" }], [{ key = "Z" }]] 10 | 11 | [controls.actions.EnableDisableSound] 12 | combinations = [[{ key = "M" }]] 13 | once = true 14 | 15 | 16 | # Usage 17 | 18 | # Axes 19 | 20 | # With keyboard keys 21 | # [controls.axes.XXX] 22 | # emulated.neg.key = "Left" 23 | # emulated.pos.key = "Right" 24 | 25 | # With mouse buttons 26 | # [controls.axes.XXX] 27 | # emulated.neg.mouse_button = "MouseButtonLeft" 28 | # emulated.pos.mouse_button = "MouseButtonRight" 29 | 30 | # With gamepad buttons 31 | # [controls.axes.XXX] 32 | # emulated.neg.controller = { id = 0, button = "GamepadButton14" } 33 | # emulated.pos.controller = { id = 0, button = "GamepadButton15" } 34 | 35 | # With gamepad axis 36 | # [controls.axes.XXX] 37 | # controller_axis = { id = 0, axis = 0, invert = false, dead_zone = 0.2 } 38 | 39 | # With mouse axis 40 | # [controls.axes.XXX] 41 | # mouse_axis.axis = 0 42 | 43 | 44 | # Actions 45 | 46 | # [controls.actions.XXX] 47 | # combinations = [ 48 | # [{ key = "ControlLeft" }, { key = "Q" }], 49 | # [{ key = "Escape" }], 50 | # [{ controller = { id = 0, button = "GamepadButton9" } }], 51 | # ] 52 | # once = true 53 | -------------------------------------------------------------------------------- /lib/components/lib.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | ecs "github.com/x-hgg-x/goecs/v2" 5 | "github.com/x-hgg-x/goecsengine/math" 6 | ) 7 | 8 | // Components contains references to all game components 9 | type Components struct { 10 | Player *ecs.NullComponent 11 | Enemy *ecs.NullComponent 12 | Controllable *ecs.SliceComponent 13 | Alien *ecs.SliceComponent 14 | AlienMaster *ecs.SliceComponent 15 | Bunker *ecs.SliceComponent 16 | Bullet *ecs.SliceComponent 17 | PlayerLine *ecs.NullComponent 18 | Deleted *ecs.NullComponent 19 | } 20 | 21 | // Player component 22 | type Player struct{} 23 | 24 | // Enemy component 25 | type Enemy struct{} 26 | 27 | // Controllable component 28 | type Controllable struct { 29 | Width float64 30 | Height float64 31 | } 32 | 33 | // Alien component 34 | type Alien struct { 35 | Width float64 36 | Height float64 37 | Translation math.Vector2 38 | } 39 | 40 | // AlienMaster component 41 | type AlienMaster struct { 42 | Direction float64 43 | } 44 | 45 | // Bunker component 46 | type Bunker struct { 47 | PixelSize int `toml:"pixel_size"` 48 | } 49 | 50 | // Bullet component 51 | type Bullet struct { 52 | Width float64 53 | Height float64 54 | Velocity float64 55 | Health float64 56 | } 57 | 58 | // PlayerLine component 59 | type PlayerLine struct{} 60 | 61 | // Deleted component 62 | type Deleted struct{} 63 | -------------------------------------------------------------------------------- /lib/loader/sound.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "github.com/x-hgg-x/goecsengine/loader" 5 | w "github.com/x-hgg-x/goecsengine/world" 6 | 7 | "github.com/hajimehoshi/ebiten/v2/audio" 8 | ) 9 | 10 | // LoadSounds loads music and sfx 11 | func LoadSounds(world w.World, sound bool) { 12 | world.Resources.AudioContext = loader.InitAudio(44100) 13 | audioPlayers := make(map[string]*audio.Player) 14 | audioPlayers["music"] = loader.LoadAudio(world.Resources.AudioContext, "assets/lfs/audio/Wave After Wave!.ogg") 15 | audioPlayers["shoot"] = loader.LoadAudio(world.Resources.AudioContext, "assets/lfs/audio/shoot.wav") 16 | audioPlayers["killed"] = loader.LoadAudio(world.Resources.AudioContext, "assets/lfs/audio/killed.wav") 17 | audioPlayers["explosion"] = loader.LoadAudio(world.Resources.AudioContext, "assets/lfs/audio/explosion.wav") 18 | world.Resources.AudioPlayers = &audioPlayers 19 | 20 | audioPlayers["music"].Play() 21 | if sound { 22 | EnableSound(world) 23 | } else { 24 | DisableSound(world) 25 | } 26 | } 27 | 28 | // EnableSound enables sound 29 | func EnableSound(world w.World) { 30 | audioPlayers := *world.Resources.AudioPlayers 31 | audioPlayers["music"].SetVolume(1) 32 | audioPlayers["shoot"].SetVolume(0.15) 33 | audioPlayers["killed"].SetVolume(0.15) 34 | audioPlayers["explosion"].SetVolume(0.15) 35 | } 36 | 37 | // DisableSound disables sound 38 | func DisableSound(world w.World) { 39 | audioPlayers := *world.Resources.AudioPlayers 40 | for key := range audioPlayers { 41 | audioPlayers[key].SetVolume(0) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/resources/game.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | // LifeEvent is triggered when the player lose a life 4 | type LifeEvent struct{} 5 | 6 | // ScoreEvent is triggered when the score changes 7 | type ScoreEvent struct { 8 | Score int 9 | } 10 | 11 | // Events contains game events for communication between game systems 12 | type Events struct { 13 | LifeEvents []LifeEvent 14 | ScoreEvents []ScoreEvent 15 | } 16 | 17 | // StateEvent is an event for game progression 18 | type StateEvent int 19 | 20 | // List of game progression events 21 | const ( 22 | StateEventNone StateEvent = iota 23 | StateEventDeath 24 | StateEventLevelComplete 25 | ) 26 | 27 | // Difficulty is a game difficulty 28 | type Difficulty float64 29 | 30 | // List of game difficulties 31 | const ( 32 | DifficultyEasy Difficulty = 0.5 33 | DifficultyNormal Difficulty = 1 34 | DifficultyHard Difficulty = 2 35 | ) 36 | 37 | // Game contains game resources 38 | type Game struct { 39 | Events Events 40 | StateEvent StateEvent 41 | Difficulty Difficulty 42 | Lives int 43 | Score int 44 | } 45 | 46 | // NewGame creates a new game 47 | func NewGame(difficulty Difficulty) *Game { 48 | return &Game{Difficulty: difficulty, Lives: 3} 49 | } 50 | 51 | // Score is a game score 52 | type Score struct { 53 | Score int 54 | Author string 55 | } 56 | 57 | // ScoreTable contains highscores for a single difficulty 58 | type ScoreTable struct { 59 | Scores []Score 60 | } 61 | 62 | // Highscores contains all highscores 63 | type Highscores struct { 64 | Easy ScoreTable 65 | Normal ScoreTable 66 | Hard ScoreTable 67 | } 68 | -------------------------------------------------------------------------------- /lib/systems/move_player.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "math" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | ec "github.com/x-hgg-x/goecsengine/components" 11 | er "github.com/x-hgg-x/goecsengine/resources" 12 | w "github.com/x-hgg-x/goecsengine/world" 13 | 14 | "github.com/hajimehoshi/ebiten/v2" 15 | ) 16 | 17 | // MovePlayerSystem moves player controllable sprite 18 | func MovePlayerSystem(world w.World) { 19 | gameComponents := world.Components.Game.(*gc.Components) 20 | 21 | world.Manager.Join(gameComponents.Player, gameComponents.Controllable, world.Components.Engine.Transform).Visit(ecs.Visit(func(entity ecs.Entity) { 22 | playerControllable := gameComponents.Controllable.Get(entity).(*gc.Controllable) 23 | playerTransform := world.Components.Engine.Transform.Get(entity).(*ec.Transform) 24 | 25 | screenWidth := float64(world.Resources.ScreenDimensions.Width) 26 | playerX := playerTransform.Translation.X 27 | axisValue := world.Resources.InputHandler.Axes[resources.PlayerAxis] 28 | 29 | if _, ok := world.Resources.Controls.Axes[resources.PlayerAxis].Value.(*er.MouseAxis); ok { 30 | playerX = (axisValue + 1) / 2 * screenWidth 31 | } else { 32 | playerX += axisValue * screenWidth / ebiten.DefaultTPS / 4 33 | } 34 | 35 | minValue := playerControllable.Width / 2 36 | maxValue := float64(world.Resources.ScreenDimensions.Width) - playerControllable.Width/2 37 | playerTransform.Translation.X = math.Min(math.Max(playerX, minValue), maxValue) 38 | })) 39 | } 40 | -------------------------------------------------------------------------------- /lib/systems/life.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "fmt" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | ec "github.com/x-hgg-x/goecsengine/components" 11 | w "github.com/x-hgg-x/goecsengine/world" 12 | ) 13 | 14 | // LifeSystem manages lives 15 | func LifeSystem(world w.World) { 16 | gameComponents := world.Components.Game.(*gc.Components) 17 | gameResources := world.Resources.Game.(*resources.Game) 18 | 19 | for range gameResources.Events.LifeEvents { 20 | gameResources.Lives-- 21 | 22 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 23 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 24 | if text.ID == "game_life" { 25 | text.Text = fmt.Sprintf("LIVES: %d", gameResources.Lives) 26 | } 27 | })) 28 | 29 | world.Manager.Join(gameComponents.Player, gameComponents.Controllable, world.Components.Engine.SpriteRender).Visit(ecs.Visit(func(playerEntity ecs.Entity) { 30 | playerSprite := world.Components.Engine.SpriteRender.Get(playerEntity).(*ec.SpriteRender) 31 | 32 | playerEntity.AddComponent(world.Components.Engine.AnimationControl, &ec.AnimationControl{ 33 | Animation: playerSprite.SpriteSheet.Animations[resources.PlayerDeathAnimation], 34 | Command: ec.AnimationCommand{Type: ec.AnimationCommandStart}, 35 | RateMultiplier: 1, 36 | }) 37 | })) 38 | 39 | gameResources.StateEvent = resources.StateEventDeath 40 | break 41 | } 42 | gameResources.Events.LifeEvents = nil 43 | } 44 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/mute_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.Text] 4 | id = "cursor_play_sound" 5 | text = "\u25ba" 6 | font_face = { font = "hack", options.size = 60.0 } 7 | color = [255, 255, 255, 255] 8 | 9 | [entity.components.UITransform] 10 | translation = { x = 45, y = 500 } 11 | 12 | 13 | [[entity]] 14 | 15 | [entity.components.Text] 16 | id = "cursor_play_muted" 17 | text = "\u25ba" 18 | font_face = { font = "hack", options.size = 60.0 } 19 | color = [255, 255, 255, 255] 20 | 21 | [entity.components.UITransform] 22 | translation = { x = 170, y = 300 } 23 | 24 | 25 | [[entity]] 26 | 27 | [entity.components.Text] 28 | id = "play_sound" 29 | text = "PLAY WITH SOUND" 30 | font_face = { font = "joystix", options.size = 60.0 } 31 | color = [255, 255, 255, 255] 32 | 33 | [entity.components.UITransform] 34 | translation = { x = 500, y = 500 } 35 | 36 | 37 | [[entity]] 38 | 39 | [entity.components.SpriteRender] 40 | fill = { width = 780, height = 60 } 41 | 42 | [entity.components.Transform] 43 | translation = { x = 500.0, y = 500.0 } 44 | 45 | [entity.components.MouseReactive] 46 | id = "play_sound" 47 | 48 | 49 | [[entity]] 50 | 51 | [entity.components.Text] 52 | id = "play_muted" 53 | text = "PLAY MUTED" 54 | font_face = { font = "joystix", options.size = 60.0 } 55 | color = [255, 255, 255, 255] 56 | 57 | [entity.components.UITransform] 58 | translation = { x = 500, y = 300 } 59 | 60 | 61 | [[entity]] 62 | 63 | [entity.components.SpriteRender] 64 | fill = { width = 530, height = 60 } 65 | 66 | [entity.components.Transform] 67 | translation = { x = 500.0, y = 300.0 } 68 | 69 | [entity.components.MouseReactive] 70 | id = "play_muted" 71 | -------------------------------------------------------------------------------- /lib/systems/spawn_alien_master.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "math/rand" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | 9 | ec "github.com/x-hgg-x/goecsengine/components" 10 | "github.com/x-hgg-x/goecsengine/loader" 11 | w "github.com/x-hgg-x/goecsengine/world" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | ) 15 | 16 | var spawnAlienMasterFrame = int(ebiten.DefaultTPS * 40 * rand.Float64()) 17 | 18 | // SpawnAlienMasterSystem spawns alien master 19 | func SpawnAlienMasterSystem(world w.World) { 20 | spawnAlienMasterFrame-- 21 | 22 | gameComponents := world.Components.Game.(*gc.Components) 23 | 24 | if !world.Manager.Join(gameComponents.AlienMaster).Empty() { 25 | return 26 | } 27 | 28 | if spawnAlienMasterFrame <= 0 { 29 | spawnAlienMasterFrame = int(ebiten.DefaultTPS * 40 * rand.Float64()) 30 | 31 | alienMasterEntity := loader.AddEntities(world, world.Resources.Prefabs.(*resources.Prefabs).Game.AlienMaster) 32 | for iEntity := range alienMasterEntity { 33 | alien := gameComponents.Alien.Get(alienMasterEntity[iEntity]).(*gc.Alien) 34 | alienMaster := gameComponents.AlienMaster.Get(alienMasterEntity[iEntity]).(*gc.AlienMaster) 35 | alienMasterTransform := world.Components.Engine.Transform.Get(alienMasterEntity[iEntity]).(*ec.Transform) 36 | 37 | if rand.Intn(2) == 0 { 38 | alienMaster.Direction = 1 39 | alienMasterTransform.Translation.X = -alien.Width / 2 40 | } else { 41 | alienMaster.Direction = -1 42 | alienMasterTransform.Translation.X = float64(world.Resources.ScreenDimensions.Width) + alien.Width/2 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/loader/lib.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "os" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | 8 | "github.com/x-hgg-x/goecsengine/loader" 9 | "github.com/x-hgg-x/goecsengine/utils" 10 | w "github.com/x-hgg-x/goecsengine/world" 11 | 12 | "github.com/BurntSushi/toml" 13 | ) 14 | 15 | type gameComponentList struct { 16 | Player *gc.Player 17 | Enemy *gc.Enemy 18 | Controllable *gc.Controllable 19 | Alien *gc.Alien 20 | AlienMaster *gc.AlienMaster 21 | Bunker *gc.Bunker 22 | Bullet *gc.Bullet 23 | PlayerLine *gc.PlayerLine 24 | Deleted *gc.Deleted 25 | } 26 | 27 | type entity struct { 28 | Components gameComponentList 29 | } 30 | 31 | type entityGameMetadata struct { 32 | Entities []entity `toml:"entity"` 33 | } 34 | 35 | func loadGameComponents(entityMetadataContent []byte, world w.World) []interface{} { 36 | var entityGameMetadata entityGameMetadata 37 | utils.Try(toml.Decode(string(entityMetadataContent), &entityGameMetadata)) 38 | 39 | gameComponentList := make([]interface{}, len(entityGameMetadata.Entities)) 40 | for iEntity, entity := range entityGameMetadata.Entities { 41 | gameComponentList[iEntity] = entity.Components 42 | } 43 | return gameComponentList 44 | } 45 | 46 | // PreloadEntities preloads entities with components 47 | func PreloadEntities(entityMetadataPath string, world w.World) loader.EntityComponentList { 48 | entityMetadataContent := utils.Try(os.ReadFile(entityMetadataPath)) 49 | 50 | return loader.EntityComponentList{ 51 | Engine: loader.LoadEngineComponents(entityMetadataContent, world), 52 | Game: loadGameComponents(entityMetadataContent, world), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/systems/shoot_player_bullet.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 5 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 6 | 7 | ecs "github.com/x-hgg-x/goecs/v2" 8 | ec "github.com/x-hgg-x/goecsengine/components" 9 | "github.com/x-hgg-x/goecsengine/loader" 10 | "github.com/x-hgg-x/goecsengine/math" 11 | w "github.com/x-hgg-x/goecsengine/world" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | ) 15 | 16 | var shootPlayerBulletFrame = 0 17 | 18 | // ShootPlayerBulletSystem shoots player bullet 19 | func ShootPlayerBulletSystem(world w.World) { 20 | shootPlayerBulletFrame-- 21 | 22 | gameComponents := world.Components.Game.(*gc.Components) 23 | audioPlayers := *world.Resources.AudioPlayers 24 | 25 | if world.Manager.Join(gameComponents.Player, gameComponents.Bullet).Empty() { 26 | shootPlayerBulletFrame = math.Min(ebiten.DefaultTPS/20, shootPlayerBulletFrame) 27 | } 28 | 29 | if world.Resources.InputHandler.Actions[resources.ShootAction] && shootPlayerBulletFrame <= 0 { 30 | shootPlayerBulletFrame = ebiten.DefaultTPS 31 | 32 | firstPlayer := ecs.GetFirst(world.Manager.Join(gameComponents.Player, gameComponents.Controllable, world.Components.Engine.Transform)) 33 | if firstPlayer == nil { 34 | return 35 | } 36 | playerX := world.Components.Engine.Transform.Get(ecs.Entity(*firstPlayer)).(*ec.Transform).Translation.X 37 | 38 | playerBulletEntity := loader.AddEntities(world, world.Resources.Prefabs.(*resources.Prefabs).Game.PlayerBullet) 39 | for iEntity := range playerBulletEntity { 40 | playerBulletTransform := world.Components.Engine.Transform.Get(playerBulletEntity[iEntity]).(*ec.Transform) 41 | playerBulletTransform.Translation.X = playerX 42 | } 43 | 44 | audioPlayers["shoot"].Rewind() 45 | audioPlayers["shoot"].Play() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/systems/shoot_enemy_bullet.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "math/rand" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | ec "github.com/x-hgg-x/goecsengine/components" 11 | "github.com/x-hgg-x/goecsengine/loader" 12 | "github.com/x-hgg-x/goecsengine/math" 13 | w "github.com/x-hgg-x/goecsengine/world" 14 | 15 | "github.com/hajimehoshi/ebiten/v2" 16 | ) 17 | 18 | var shootEnemyBulletFrame = ebiten.DefaultTPS 19 | 20 | // ShootEnemyBulletSystem shoots enemy bullet 21 | func ShootEnemyBulletSystem(world w.World) { 22 | shootEnemyBulletFrame-- 23 | 24 | gameComponents := world.Components.Game.(*gc.Components) 25 | gameResources := world.Resources.Game.(*resources.Game) 26 | 27 | alienSet := world.Manager.Join(gameComponents.Alien, gameComponents.AlienMaster.Not()) 28 | if alienSet.Empty() { 29 | return 30 | } 31 | 32 | if shootEnemyBulletFrame <= 0 { 33 | shootEnemyBulletFrame = int(ebiten.DefaultTPS / float64(gameResources.Difficulty) * rand.Float64()) 34 | 35 | // Select random alien 36 | alienEntities := []ecs.Entity{} 37 | alienSet.Visit(ecs.Visit(func(entity ecs.Entity) { 38 | alienEntities = append(alienEntities, entity) 39 | })) 40 | alienEntity := alienEntities[rand.Intn(len(alienEntities))] 41 | alienHeight := gameComponents.Alien.Get(alienEntity).(*gc.Alien).Height 42 | alienTranslation := world.Components.Engine.Transform.Get(alienEntity).(*ec.Transform).Translation 43 | 44 | enemyBulletEntity := loader.AddEntities(world, world.Resources.Prefabs.(*resources.Prefabs).Game.EnemyBullet) 45 | for iEntity := range enemyBulletEntity { 46 | enemyBulletHeight := gameComponents.Bullet.Get(enemyBulletEntity[iEntity]).(*gc.Bullet).Height 47 | enemyBulletTransform := world.Components.Engine.Transform.Get(enemyBulletEntity[iEntity]).(*ec.Transform) 48 | enemyBulletTransform.Translation = math.Vector2{ 49 | X: alienTranslation.X, 50 | Y: alienTranslation.Y - alienHeight/2 - enemyBulletHeight/2, 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/level_complete_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | fill = { width = 1000, height = 800, color = [0, 0, 0, 120] } 5 | 6 | [entity.components.Transform] 7 | translation = { x = 500.0, y = 400.0 } 8 | depth = 1.0 9 | 10 | 11 | [[entity]] 12 | 13 | [entity.components.Text] 14 | id = "level_complete" 15 | text = "LEVEL COMPLETE" 16 | font_face = { font = "joystix", options.size = 80.0 } 17 | color = [255, 255, 255, 255] 18 | 19 | [entity.components.UITransform] 20 | translation = { x = 500, y = 600 } 21 | 22 | 23 | [[entity]] 24 | 25 | [entity.components.Text] 26 | id = "cursor_continue" 27 | text = "\u25ba" 28 | font_face = { font = "hack", options.size = 60.0 } 29 | color = [255, 255, 255, 255] 30 | 31 | [entity.components.UITransform] 32 | translation = { x = 235, y = 400 } 33 | 34 | 35 | [[entity]] 36 | 37 | [entity.components.Text] 38 | id = "cursor_main_menu" 39 | text = "\u25ba" 40 | font_face = { font = "hack", options.size = 60.0 } 41 | color = [255, 255, 255, 255] 42 | 43 | [entity.components.UITransform] 44 | translation = { x = 210, y = 250 } 45 | 46 | 47 | [[entity]] 48 | 49 | [entity.components.Text] 50 | id = "continue" 51 | text = "CONTINUE" 52 | font_face = { font = "joystix", options.size = 60.0 } 53 | color = [255, 255, 255, 255] 54 | 55 | [entity.components.UITransform] 56 | translation = { x = 500, y = 400 } 57 | 58 | 59 | [[entity]] 60 | 61 | [entity.components.SpriteRender] 62 | fill = { width = 430, height = 60 } 63 | 64 | [entity.components.Transform] 65 | translation = { x = 500.0, y = 400.0 } 66 | 67 | [entity.components.MouseReactive] 68 | id = "continue" 69 | 70 | 71 | [[entity]] 72 | 73 | [entity.components.Text] 74 | id = "main_menu" 75 | text = "MAIN MENU" 76 | font_face = { font = "joystix", options.size = 60.0 } 77 | color = [255, 255, 255, 255] 78 | 79 | [entity.components.UITransform] 80 | translation = { x = 500, y = 250 } 81 | 82 | 83 | [[entity]] 84 | 85 | [entity.components.SpriteRender] 86 | fill = { width = 480, height = 60 } 87 | 88 | [entity.components.Transform] 89 | translation = { x = 500.0, y = 250.0 } 90 | 91 | [entity.components.MouseReactive] 92 | id = "main_menu" 93 | -------------------------------------------------------------------------------- /lib/states/mute_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 7 | 8 | "github.com/x-hgg-x/goecsengine/loader" 9 | "github.com/x-hgg-x/goecsengine/states" 10 | w "github.com/x-hgg-x/goecsengine/world" 11 | 12 | "github.com/hajimehoshi/ebiten/v2" 13 | "github.com/hajimehoshi/ebiten/v2/inpututil" 14 | ) 15 | 16 | // MuteMenuState is the mute menu state 17 | type MuteMenuState struct { 18 | selection int 19 | } 20 | 21 | // 22 | // Menu interface 23 | // 24 | 25 | func (st *MuteMenuState) getSelection() int { 26 | return st.selection 27 | } 28 | 29 | func (st *MuteMenuState) setSelection(selection int) { 30 | st.selection = selection 31 | } 32 | 33 | func (st *MuteMenuState) confirmSelection() states.Transition { 34 | switch st.selection { 35 | case 0: 36 | // Play with sound 37 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&MainMenuState{sound: true}}} 38 | case 1: 39 | // Play muted 40 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&MainMenuState{sound: false}}} 41 | } 42 | panic(fmt.Errorf("unknown selection: %d", st.selection)) 43 | } 44 | 45 | func (st *MuteMenuState) getMenuIDs() []string { 46 | return []string{"play_sound", "play_muted"} 47 | } 48 | 49 | func (st *MuteMenuState) getCursorMenuIDs() []string { 50 | return []string{"cursor_play_sound", "cursor_play_muted"} 51 | } 52 | 53 | // 54 | // State interface 55 | // 56 | 57 | // OnPause method 58 | func (st *MuteMenuState) OnPause(world w.World) {} 59 | 60 | // OnResume method 61 | func (st *MuteMenuState) OnResume(world w.World) {} 62 | 63 | // OnStart method 64 | func (st *MuteMenuState) OnStart(world w.World) { 65 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 66 | loader.AddEntities(world, prefabs.Menu.MuteMenu) 67 | } 68 | 69 | // OnStop method 70 | func (st *MuteMenuState) OnStop(world w.World) { 71 | world.Manager.DeleteAllEntities() 72 | } 73 | 74 | // Update method 75 | func (st *MuteMenuState) Update(world w.World) states.Transition { 76 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 77 | return states.Transition{Type: states.TransQuit} 78 | } 79 | return updateMenu(st, world) 80 | } 81 | -------------------------------------------------------------------------------- /lib/states/game_over_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 7 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | "github.com/x-hgg-x/goecsengine/loader" 11 | "github.com/x-hgg-x/goecsengine/states" 12 | w "github.com/x-hgg-x/goecsengine/world" 13 | ) 14 | 15 | // GameOverState is the game over menu state 16 | type GameOverState struct { 17 | difficulty resources.Difficulty 18 | gameOverMenu []ecs.Entity 19 | selection int 20 | } 21 | 22 | // 23 | // Menu interface 24 | // 25 | 26 | func (st *GameOverState) getSelection() int { 27 | return st.selection 28 | } 29 | 30 | func (st *GameOverState) setSelection(selection int) { 31 | st.selection = selection 32 | } 33 | 34 | func (st *GameOverState) confirmSelection() states.Transition { 35 | switch st.selection { 36 | case 0: 37 | // Restart 38 | return states.Transition{Type: states.TransReplace, NewStates: []states.State{&GameplayState{game: resources.NewGame(st.difficulty)}}} 39 | case 1: 40 | // Main Menu 41 | return states.Transition{Type: states.TransReplace, NewStates: []states.State{&MainMenuState{}}} 42 | case 2: 43 | // Exit 44 | return states.Transition{Type: states.TransQuit} 45 | } 46 | panic(fmt.Errorf("unknown selection: %d", st.selection)) 47 | } 48 | 49 | func (st *GameOverState) getMenuIDs() []string { 50 | return []string{"restart", "main_menu", "exit"} 51 | } 52 | 53 | func (st *GameOverState) getCursorMenuIDs() []string { 54 | return []string{"cursor_restart", "cursor_main_menu", "cursor_exit"} 55 | } 56 | 57 | // 58 | // State interface 59 | // 60 | 61 | // OnPause method 62 | func (st *GameOverState) OnPause(world w.World) {} 63 | 64 | // OnResume method 65 | func (st *GameOverState) OnResume(world w.World) {} 66 | 67 | // OnStart method 68 | func (st *GameOverState) OnStart(world w.World) { 69 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 70 | st.gameOverMenu = append(st.gameOverMenu, loader.AddEntities(world, prefabs.Menu.GameOverMenu)...) 71 | } 72 | 73 | // OnStop method 74 | func (st *GameOverState) OnStop(world w.World) { 75 | world.Manager.DeleteEntities(st.gameOverMenu...) 76 | } 77 | 78 | // Update method 79 | func (st *GameOverState) Update(world w.World) states.Transition { 80 | g.SoundSystem(world) 81 | 82 | return updateMenu(st, world) 83 | } 84 | -------------------------------------------------------------------------------- /lib/states/pause_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 7 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | "github.com/x-hgg-x/goecsengine/loader" 11 | "github.com/x-hgg-x/goecsengine/states" 12 | w "github.com/x-hgg-x/goecsengine/world" 13 | 14 | "github.com/hajimehoshi/ebiten/v2" 15 | "github.com/hajimehoshi/ebiten/v2/inpututil" 16 | ) 17 | 18 | // PauseMenuState is the pause menu state 19 | type PauseMenuState struct { 20 | pauseMenu []ecs.Entity 21 | selection int 22 | } 23 | 24 | // 25 | // Menu interface 26 | // 27 | 28 | func (st *PauseMenuState) getSelection() int { 29 | return st.selection 30 | } 31 | 32 | func (st *PauseMenuState) setSelection(selection int) { 33 | st.selection = selection 34 | } 35 | 36 | func (st *PauseMenuState) confirmSelection() states.Transition { 37 | switch st.selection { 38 | case 0: 39 | // Resume 40 | return states.Transition{Type: states.TransPop} 41 | case 1: 42 | // Main Menu 43 | return states.Transition{Type: states.TransReplace, NewStates: []states.State{&MainMenuState{}}} 44 | case 2: 45 | // Exit 46 | return states.Transition{Type: states.TransQuit} 47 | } 48 | panic(fmt.Errorf("unknown selection: %d", st.selection)) 49 | } 50 | 51 | func (st *PauseMenuState) getMenuIDs() []string { 52 | return []string{"resume", "main_menu", "exit"} 53 | } 54 | 55 | func (st *PauseMenuState) getCursorMenuIDs() []string { 56 | return []string{"cursor_resume", "cursor_main_menu", "cursor_exit"} 57 | } 58 | 59 | // 60 | // State interface 61 | // 62 | 63 | // OnPause method 64 | func (st *PauseMenuState) OnPause(world w.World) {} 65 | 66 | // OnResume method 67 | func (st *PauseMenuState) OnResume(world w.World) {} 68 | 69 | // OnStart method 70 | func (st *PauseMenuState) OnStart(world w.World) { 71 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 72 | st.pauseMenu = append(st.pauseMenu, loader.AddEntities(world, prefabs.Menu.PauseMenu)...) 73 | } 74 | 75 | // OnStop method 76 | func (st *PauseMenuState) OnStop(world w.World) { 77 | world.Manager.DeleteEntities(st.pauseMenu...) 78 | } 79 | 80 | // Update method 81 | func (st *PauseMenuState) Update(world w.World) states.Transition { 82 | g.SoundSystem(world) 83 | 84 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 85 | return states.Transition{Type: states.TransPop} 86 | } 87 | return updateMenu(st, world) 88 | } 89 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/difficulty_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.Text] 4 | id = "cursor_easy" 5 | text = "\u25ba" 6 | font_face = { font = "hack", options.size = 60.0 } 7 | color = [255, 255, 255, 255] 8 | 9 | [entity.components.UITransform] 10 | translation = { x = 340, y = 550 } 11 | 12 | 13 | [[entity]] 14 | 15 | [entity.components.Text] 16 | id = "cursor_normal" 17 | text = "\u25ba" 18 | font_face = { font = "hack", options.size = 60.0 } 19 | color = [255, 255, 255, 255] 20 | 21 | [entity.components.UITransform] 22 | translation = { x = 285, y = 400 } 23 | 24 | 25 | [[entity]] 26 | 27 | [entity.components.Text] 28 | id = "cursor_hard" 29 | text = "\u25ba" 30 | font_face = { font = "hack", options.size = 60.0 } 31 | color = [255, 255, 255, 255] 32 | 33 | [entity.components.UITransform] 34 | translation = { x = 340, y = 250 } 35 | 36 | 37 | [[entity]] 38 | 39 | [entity.components.Text] 40 | id = "easy" 41 | text = "EASY" 42 | font_face = { font = "joystix", options.size = 60.0 } 43 | color = [255, 255, 255, 255] 44 | 45 | [entity.components.UITransform] 46 | translation = { x = 500, y = 550 } 47 | 48 | 49 | [[entity]] 50 | 51 | [entity.components.SpriteRender] 52 | fill = { width = 220, height = 60 } 53 | 54 | [entity.components.Transform] 55 | translation = { x = 500.0, y = 550.0 } 56 | 57 | [entity.components.MouseReactive] 58 | id = "easy" 59 | 60 | 61 | [[entity]] 62 | 63 | [entity.components.Text] 64 | id = "normal" 65 | text = "NORMAL" 66 | font_face = { font = "joystix", options.size = 60.0 } 67 | color = [255, 255, 255, 255] 68 | 69 | [entity.components.UITransform] 70 | translation = { x = 500, y = 400 } 71 | 72 | 73 | [[entity]] 74 | 75 | [entity.components.SpriteRender] 76 | fill = { width = 330, height = 60 } 77 | 78 | [entity.components.Transform] 79 | translation = { x = 500.0, y = 400.0 } 80 | 81 | [entity.components.MouseReactive] 82 | id = "normal" 83 | 84 | 85 | [[entity]] 86 | 87 | [entity.components.Text] 88 | id = "hard" 89 | text = "HARD" 90 | font_face = { font = "joystix", options.size = 60.0 } 91 | color = [255, 255, 255, 255] 92 | 93 | [entity.components.UITransform] 94 | translation = { x = 500, y = 250 } 95 | 96 | 97 | [[entity]] 98 | 99 | [entity.components.SpriteRender] 100 | fill = { width = 220, height = 60 } 101 | 102 | [entity.components.Transform] 103 | translation = { x = 500.0, y = 250.0 } 104 | 105 | [entity.components.MouseReactive] 106 | id = "hard" 107 | -------------------------------------------------------------------------------- /lib/states/level_complete_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 7 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | "github.com/x-hgg-x/goecsengine/loader" 11 | "github.com/x-hgg-x/goecsengine/states" 12 | w "github.com/x-hgg-x/goecsengine/world" 13 | ) 14 | 15 | // LevelCompleteState is the level complete menu state 16 | type LevelCompleteState struct { 17 | game *resources.Game 18 | levelCompleteMenu []ecs.Entity 19 | selection int 20 | } 21 | 22 | // 23 | // Menu interface 24 | // 25 | 26 | func (st *LevelCompleteState) getSelection() int { 27 | return st.selection 28 | } 29 | 30 | func (st *LevelCompleteState) setSelection(selection int) { 31 | st.selection = selection 32 | } 33 | 34 | func (st *LevelCompleteState) confirmSelection() states.Transition { 35 | switch st.selection { 36 | case 0: 37 | // Continue 38 | return states.Transition{Type: states.TransReplace, NewStates: []states.State{&GameplayState{game: st.game}}} 39 | case 1: 40 | // Main Menu 41 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&HighscoresState{ 42 | newScore: &highscore{difficulty: st.game.Difficulty, score: st.game.Score}, 43 | exitTransition: states.Transition{Type: states.TransReplace, NewStates: []states.State{&MainMenuState{}}}, 44 | }}} 45 | } 46 | panic(fmt.Errorf("unknown selection: %d", st.selection)) 47 | } 48 | 49 | func (st *LevelCompleteState) getMenuIDs() []string { 50 | return []string{"continue", "main_menu"} 51 | } 52 | 53 | func (st *LevelCompleteState) getCursorMenuIDs() []string { 54 | return []string{"cursor_continue", "cursor_main_menu"} 55 | } 56 | 57 | // 58 | // State interface 59 | // 60 | 61 | // OnPause method 62 | func (st *LevelCompleteState) OnPause(world w.World) {} 63 | 64 | // OnResume method 65 | func (st *LevelCompleteState) OnResume(world w.World) {} 66 | 67 | // OnStart method 68 | func (st *LevelCompleteState) OnStart(world w.World) { 69 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 70 | st.levelCompleteMenu = append(st.levelCompleteMenu, loader.AddEntities(world, prefabs.Menu.LevelCompleteMenu)...) 71 | } 72 | 73 | // OnStop method 74 | func (st *LevelCompleteState) OnStop(world w.World) { 75 | world.Manager.DeleteEntities(st.levelCompleteMenu...) 76 | } 77 | 78 | // Update method 79 | func (st *LevelCompleteState) Update(world w.World) states.Transition { 80 | g.SoundSystem(world) 81 | 82 | return updateMenu(st, world) 83 | } 84 | -------------------------------------------------------------------------------- /lib/states/menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "github.com/x-hgg-x/space-invaders-go/lib/math" 5 | 6 | ecs "github.com/x-hgg-x/goecs/v2" 7 | ec "github.com/x-hgg-x/goecsengine/components" 8 | m "github.com/x-hgg-x/goecsengine/math" 9 | "github.com/x-hgg-x/goecsengine/states" 10 | w "github.com/x-hgg-x/goecsengine/world" 11 | 12 | "github.com/hajimehoshi/ebiten/v2" 13 | "github.com/hajimehoshi/ebiten/v2/inpututil" 14 | ) 15 | 16 | type menu interface { 17 | getSelection() int 18 | setSelection(selection int) 19 | confirmSelection() states.Transition 20 | getMenuIDs() []string 21 | getCursorMenuIDs() []string 22 | } 23 | 24 | var menuLastCursorPosition = m.VectorInt2{} 25 | 26 | func updateMenu(menu menu, world w.World) states.Transition { 27 | var transition states.Transition 28 | selection := menu.getSelection() 29 | numItems := len(menu.getCursorMenuIDs()) 30 | 31 | // Handle keyboard events 32 | switch { 33 | case inpututil.IsKeyJustPressed(ebiten.KeyDown): 34 | menu.setSelection(math.Mod(selection+1, numItems)) 35 | case inpututil.IsKeyJustPressed(ebiten.KeyUp): 36 | menu.setSelection(math.Mod(selection-1, numItems)) 37 | case inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace): 38 | return menu.confirmSelection() 39 | } 40 | 41 | // Handle mouse events only if mouse is moved or clicked 42 | x, y := ebiten.CursorPosition() 43 | if x != menuLastCursorPosition.X || y != menuLastCursorPosition.Y || inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { 44 | menuLastCursorPosition = m.VectorInt2{X: x, Y: y} 45 | 46 | for iElem, id := range menu.getMenuIDs() { 47 | if world.Manager.Join(world.Components.Engine.SpriteRender, world.Components.Engine.Transform, world.Components.Engine.MouseReactive).Visit( 48 | func(index int) (skip bool) { 49 | mouseReactive := world.Components.Engine.MouseReactive.Get(ecs.Entity(index)).(*ec.MouseReactive) 50 | if mouseReactive.ID == id && mouseReactive.Hovered { 51 | menu.setSelection(iElem) 52 | if mouseReactive.JustClicked { 53 | transition = menu.confirmSelection() 54 | return true 55 | } 56 | } 57 | return false 58 | }) { 59 | return transition 60 | } 61 | } 62 | } 63 | 64 | // Set cursor color 65 | newSelection := menu.getSelection() 66 | for iCursor, id := range menu.getCursorMenuIDs() { 67 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 68 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 69 | if text.ID == id { 70 | text.Color.A = 0 71 | if iCursor == newSelection { 72 | text.Color.A = 255 73 | } 74 | } 75 | })) 76 | } 77 | return transition 78 | } 79 | -------------------------------------------------------------------------------- /lib/states/main_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | gloader "github.com/x-hgg-x/space-invaders-go/lib/loader" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 9 | 10 | "github.com/x-hgg-x/goecsengine/loader" 11 | "github.com/x-hgg-x/goecsengine/states" 12 | w "github.com/x-hgg-x/goecsengine/world" 13 | 14 | "github.com/hajimehoshi/ebiten/v2" 15 | "github.com/hajimehoshi/ebiten/v2/inpututil" 16 | ) 17 | 18 | // MainMenuState is the main menu state 19 | type MainMenuState struct { 20 | selection int 21 | sound bool 22 | } 23 | 24 | // 25 | // Menu interface 26 | // 27 | 28 | func (st *MainMenuState) getSelection() int { 29 | return st.selection 30 | } 31 | 32 | func (st *MainMenuState) setSelection(selection int) { 33 | st.selection = selection 34 | } 35 | 36 | func (st *MainMenuState) confirmSelection() states.Transition { 37 | switch st.selection { 38 | case 0: 39 | // New game 40 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&DifficultyMenuState{}}} 41 | case 1: 42 | // Highscores 43 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&HighscoresState{ 44 | exitTransition: states.Transition{Type: states.TransSwitch, NewStates: []states.State{&MainMenuState{}}}, 45 | }}} 46 | case 2: 47 | // Exit 48 | return states.Transition{Type: states.TransQuit} 49 | } 50 | panic(fmt.Errorf("unknown selection: %d", st.selection)) 51 | } 52 | 53 | func (st *MainMenuState) getMenuIDs() []string { 54 | return []string{"new_game", "highscores", "exit"} 55 | } 56 | 57 | func (st *MainMenuState) getCursorMenuIDs() []string { 58 | return []string{"cursor_new_game", "cursor_highscores", "cursor_exit"} 59 | } 60 | 61 | // 62 | // State interface 63 | // 64 | 65 | // OnPause method 66 | func (st *MainMenuState) OnPause(world w.World) {} 67 | 68 | // OnResume method 69 | func (st *MainMenuState) OnResume(world w.World) {} 70 | 71 | // OnStart method 72 | func (st *MainMenuState) OnStart(world w.World) { 73 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 74 | loader.AddEntities(world, prefabs.Game.Background) 75 | loader.AddEntities(world, prefabs.Menu.MainMenu) 76 | 77 | // Load music and sfx (at game start only) 78 | if world.Resources.AudioContext == nil { 79 | gloader.LoadSounds(world, st.sound) 80 | } 81 | } 82 | 83 | // OnStop method 84 | func (st *MainMenuState) OnStop(world w.World) { 85 | world.Manager.DeleteAllEntities() 86 | } 87 | 88 | // Update method 89 | func (st *MainMenuState) Update(world w.World) states.Transition { 90 | g.SoundSystem(world) 91 | 92 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 93 | return states.Transition{Type: states.TransQuit} 94 | } 95 | return updateMenu(st, world) 96 | } 97 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/pause_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | fill = { width = 1000, height = 800, color = [0, 0, 0, 120] } 5 | 6 | [entity.components.Transform] 7 | translation = { x = 0.0, y = 0.0 } 8 | origin = "Middle" 9 | depth = 1.0 10 | 11 | 12 | [[entity]] 13 | 14 | [entity.components.Text] 15 | id = "cursor_resume" 16 | text = "\u25ba" 17 | font_face = { font = "hack", options.size = 60.0 } 18 | color = [255, 255, 255, 255] 19 | 20 | [entity.components.UITransform] 21 | translation = { x = 290, y = 550 } 22 | 23 | 24 | [[entity]] 25 | 26 | [entity.components.Text] 27 | id = "cursor_main_menu" 28 | text = "\u25ba" 29 | font_face = { font = "hack", options.size = 60.0 } 30 | color = [255, 255, 255, 255] 31 | 32 | [entity.components.UITransform] 33 | translation = { x = 210, y = 400 } 34 | 35 | 36 | [[entity]] 37 | 38 | [entity.components.Text] 39 | id = "cursor_exit" 40 | text = "\u25ba" 41 | font_face = { font = "hack", options.size = 60.0 } 42 | color = [255, 255, 255, 255] 43 | 44 | [entity.components.UITransform] 45 | translation = { x = 340, y = 250 } 46 | 47 | 48 | [[entity]] 49 | 50 | [entity.components.Text] 51 | id = "resume" 52 | text = "RESUME" 53 | font_face = { font = "joystix", options.size = 60.0 } 54 | color = [255, 255, 255, 255] 55 | 56 | [entity.components.UITransform] 57 | translation = { x = 500, y = 550 } 58 | 59 | 60 | [[entity]] 61 | 62 | [entity.components.SpriteRender] 63 | fill = { width = 320, height = 60 } 64 | 65 | [entity.components.Transform] 66 | translation = { x = 500.0, y = 550.0 } 67 | 68 | [entity.components.MouseReactive] 69 | id = "resume" 70 | 71 | 72 | [[entity]] 73 | 74 | [entity.components.Text] 75 | id = "main_menu" 76 | text = "MAIN MENU" 77 | font_face = { font = "joystix", options.size = 60.0 } 78 | color = [255, 255, 255, 255] 79 | 80 | [entity.components.UITransform] 81 | translation = { x = 500, y = 400 } 82 | 83 | 84 | [[entity]] 85 | 86 | [entity.components.SpriteRender] 87 | fill = { width = 480, height = 60 } 88 | 89 | [entity.components.Transform] 90 | translation = { x = 500.0, y = 400.0 } 91 | 92 | [entity.components.MouseReactive] 93 | id = "main_menu" 94 | 95 | 96 | [[entity]] 97 | 98 | [entity.components.Text] 99 | id = "exit" 100 | text = "EXIT" 101 | font_face = { font = "joystix", options.size = 60.0 } 102 | color = [255, 255, 255, 255] 103 | 104 | [entity.components.UITransform] 105 | translation = { x = 500, y = 250 } 106 | 107 | 108 | [[entity]] 109 | 110 | [entity.components.SpriteRender] 111 | fill = { width = 220, height = 60 } 112 | 113 | [entity.components.Transform] 114 | translation = { x = 500.0, y = 250.0 } 115 | 116 | [entity.components.MouseReactive] 117 | id = "exit" 118 | -------------------------------------------------------------------------------- /lib/states/difficulty_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 7 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 8 | 9 | "github.com/x-hgg-x/goecsengine/loader" 10 | "github.com/x-hgg-x/goecsengine/states" 11 | w "github.com/x-hgg-x/goecsengine/world" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | "github.com/hajimehoshi/ebiten/v2/inpututil" 15 | ) 16 | 17 | // DifficultyMenuState is the difficulty menu state 18 | type DifficultyMenuState struct { 19 | selection int 20 | } 21 | 22 | // 23 | // Menu interface 24 | // 25 | 26 | func (st *DifficultyMenuState) getSelection() int { 27 | return st.selection 28 | } 29 | 30 | func (st *DifficultyMenuState) setSelection(selection int) { 31 | st.selection = selection 32 | } 33 | 34 | func (st *DifficultyMenuState) confirmSelection() states.Transition { 35 | switch st.selection { 36 | case 0: 37 | // Easy 38 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&GameplayState{game: resources.NewGame(resources.DifficultyEasy)}}} 39 | case 1: 40 | // Normal 41 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&GameplayState{game: resources.NewGame(resources.DifficultyNormal)}}} 42 | case 2: 43 | // Hard 44 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&GameplayState{game: resources.NewGame(resources.DifficultyHard)}}} 45 | } 46 | panic(fmt.Errorf("unknown selection: %d", st.selection)) 47 | } 48 | 49 | func (st *DifficultyMenuState) getMenuIDs() []string { 50 | return []string{"easy", "normal", "hard"} 51 | } 52 | 53 | func (st *DifficultyMenuState) getCursorMenuIDs() []string { 54 | return []string{"cursor_easy", "cursor_normal", "cursor_hard"} 55 | } 56 | 57 | // 58 | // State interface 59 | // 60 | 61 | // OnPause method 62 | func (st *DifficultyMenuState) OnPause(world w.World) {} 63 | 64 | // OnResume method 65 | func (st *DifficultyMenuState) OnResume(world w.World) {} 66 | 67 | // OnStart method 68 | func (st *DifficultyMenuState) OnStart(world w.World) { 69 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 70 | loader.AddEntities(world, prefabs.Game.Background) 71 | loader.AddEntities(world, prefabs.Menu.DifficultyMenu) 72 | 73 | // Default difficulty is normal 74 | st.setSelection(1) 75 | } 76 | 77 | // OnStop method 78 | func (st *DifficultyMenuState) OnStop(world w.World) { 79 | world.Manager.DeleteAllEntities() 80 | } 81 | 82 | // Update method 83 | func (st *DifficultyMenuState) Update(world w.World) states.Transition { 84 | g.SoundSystem(world) 85 | 86 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 87 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&MainMenuState{}}} 88 | } 89 | return updateMenu(st, world) 90 | } 91 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/main_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.Text] 4 | id = "title" 5 | text = "Space Invaders" 6 | font_face = { font = "joystix", options.size = 75.0 } 7 | color = [255, 255, 255, 255] 8 | 9 | [entity.components.UITransform] 10 | translation = { x = 500, y = 650 } 11 | 12 | 13 | [[entity]] 14 | 15 | [entity.components.Text] 16 | id = "cursor_new_game" 17 | text = "\u25ba" 18 | font_face = { font = "hack", options.size = 60.0 } 19 | color = [255, 255, 255, 255] 20 | 21 | [entity.components.UITransform] 22 | translation = { x = 230, y = 450 } 23 | 24 | 25 | [[entity]] 26 | 27 | [entity.components.Text] 28 | id = "cursor_highscores" 29 | text = "\u25ba" 30 | font_face = { font = "hack", options.size = 60.0 } 31 | color = [255, 255, 255, 255] 32 | 33 | [entity.components.UITransform] 34 | translation = { x = 180, y = 300 } 35 | 36 | 37 | [[entity]] 38 | 39 | [entity.components.Text] 40 | id = "cursor_exit" 41 | text = "\u25ba" 42 | font_face = { font = "hack", options.size = 60.0 } 43 | color = [255, 255, 255, 255] 44 | 45 | [entity.components.UITransform] 46 | translation = { x = 330, y = 150 } 47 | 48 | 49 | [[entity]] 50 | 51 | [entity.components.Text] 52 | id = "new_game" 53 | text = "NEW GAME" 54 | font_face = { font = "joystix", options.size = 60.0 } 55 | color = [255, 255, 255, 255] 56 | 57 | [entity.components.UITransform] 58 | translation = { x = 500, y = 450 } 59 | 60 | 61 | [[entity]] 62 | 63 | [entity.components.SpriteRender] 64 | fill = { width = 420, height = 60 } 65 | 66 | [entity.components.Transform] 67 | translation = { x = 500.0, y = 450.0 } 68 | 69 | [entity.components.MouseReactive] 70 | id = "new_game" 71 | 72 | 73 | [[entity]] 74 | 75 | [entity.components.Text] 76 | id = "highscores" 77 | text = "HIGHSCORES" 78 | font_face = { font = "joystix", options.size = 60.0 } 79 | color = [255, 255, 255, 255] 80 | 81 | [entity.components.UITransform] 82 | translation = { x = 500, y = 300 } 83 | 84 | 85 | [[entity]] 86 | 87 | [entity.components.SpriteRender] 88 | fill = { width = 520, height = 60 } 89 | 90 | [entity.components.Transform] 91 | translation = { x = 500.0, y = 300.0 } 92 | 93 | [entity.components.MouseReactive] 94 | id = "highscores" 95 | 96 | 97 | [[entity]] 98 | 99 | [entity.components.Text] 100 | id = "exit" 101 | text = "EXIT" 102 | font_face = { font = "joystix", options.size = 60.0 } 103 | color = [255, 255, 255, 255] 104 | 105 | [entity.components.UITransform] 106 | translation = { x = 500, y = 150 } 107 | 108 | 109 | [[entity]] 110 | 111 | [entity.components.SpriteRender] 112 | fill = { width = 220, height = 60 } 113 | 114 | [entity.components.Transform] 115 | translation = { x = 500.0, y = 150.0 } 116 | 117 | [entity.components.MouseReactive] 118 | id = "exit" 119 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/game_over_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | fill = { width = 1000, height = 800, color = [0, 0, 0, 120] } 5 | 6 | [entity.components.Transform] 7 | translation = { x = 500.0, y = 400.0 } 8 | depth = 1.0 9 | 10 | 11 | [[entity]] 12 | 13 | [entity.components.Text] 14 | id = "game_over" 15 | text = "GAME OVER" 16 | font_face = { font = "joystix", options.size = 80.0 } 17 | color = [255, 255, 255, 255] 18 | 19 | [entity.components.UITransform] 20 | translation = { x = 500, y = 650 } 21 | 22 | 23 | [[entity]] 24 | 25 | [entity.components.Text] 26 | id = "cursor_restart" 27 | text = "\u25ba" 28 | font_face = { font = "hack", options.size = 60.0 } 29 | color = [255, 255, 255, 255] 30 | 31 | [entity.components.UITransform] 32 | translation = { x = 265, y = 450 } 33 | 34 | 35 | [[entity]] 36 | 37 | [entity.components.Text] 38 | id = "cursor_main_menu" 39 | text = "\u25ba" 40 | font_face = { font = "hack", options.size = 60.0 } 41 | color = [255, 255, 255, 255] 42 | 43 | [entity.components.UITransform] 44 | translation = { x = 210, y = 300 } 45 | 46 | 47 | [[entity]] 48 | 49 | [entity.components.Text] 50 | id = "cursor_exit" 51 | text = "\u25ba" 52 | font_face = { font = "hack", options.size = 60.0 } 53 | color = [255, 255, 255, 255] 54 | 55 | [entity.components.UITransform] 56 | translation = { x = 340, y = 150 } 57 | 58 | 59 | [[entity]] 60 | 61 | [entity.components.Text] 62 | id = "restart" 63 | text = "RESTART" 64 | font_face = { font = "joystix", options.size = 60.0 } 65 | color = [255, 255, 255, 255] 66 | 67 | [entity.components.UITransform] 68 | translation = { x = 500, y = 450 } 69 | 70 | 71 | [[entity]] 72 | 73 | [entity.components.SpriteRender] 74 | fill = { width = 370, height = 60 } 75 | 76 | [entity.components.Transform] 77 | translation = { x = 500.0, y = 450.0 } 78 | 79 | [entity.components.MouseReactive] 80 | id = "restart" 81 | 82 | 83 | [[entity]] 84 | 85 | [entity.components.Text] 86 | id = "main_menu" 87 | text = "MAIN MENU" 88 | font_face = { font = "joystix", options.size = 60.0 } 89 | color = [255, 255, 255, 255] 90 | 91 | [entity.components.UITransform] 92 | translation = { x = 500, y = 300 } 93 | 94 | 95 | [[entity]] 96 | 97 | [entity.components.SpriteRender] 98 | fill = { width = 480, height = 60 } 99 | 100 | [entity.components.Transform] 101 | translation = { x = 500.0, y = 300.0 } 102 | 103 | [entity.components.MouseReactive] 104 | id = "main_menu" 105 | 106 | 107 | [[entity]] 108 | 109 | [entity.components.Text] 110 | id = "exit" 111 | text = "EXIT" 112 | font_face = { font = "joystix", options.size = 60.0 } 113 | color = [255, 255, 255, 255] 114 | 115 | [entity.components.UITransform] 116 | translation = { x = 500, y = 150 } 117 | 118 | 119 | [[entity]] 120 | 121 | [entity.components.SpriteRender] 122 | fill = { width = 220, height = 60 } 123 | 124 | [entity.components.Transform] 125 | translation = { x = 500.0, y = 150.0 } 126 | 127 | [entity.components.MouseReactive] 128 | id = "exit" 129 | -------------------------------------------------------------------------------- /assets/metadata/spritesheets/spritesheets.toml: -------------------------------------------------------------------------------- 1 | [sprite_sheet.background] 2 | texture_image = "assets/lfs/textures/background.png" 3 | sprites = [{ x = 0, y = 0, width = 1000, height = 800 }] 4 | 5 | [sprite_sheet.bunker] 6 | texture_image = "assets/lfs/textures/bunker.png" 7 | sprites = [{ x = 0, y = 0, width = 168, height = 120 }] 8 | 9 | [sprite_sheet.game] 10 | texture_image = "assets/lfs/textures/game.png" 11 | sprites = [ 12 | # Alien 1 13 | { x = 0, y = 0, width = 72, height = 48 }, # Sprite 0 14 | { x = 72, y = 0, width = 72, height = 48 }, # Sprite 1 15 | { x = 144, y = 0, width = 72, height = 48 }, # Sprite 2 16 | # Alien 2 17 | { x = 0, y = 48, width = 72, height = 48 }, # Sprite 3 18 | { x = 72, y = 48, width = 72, height = 48 }, # Sprite 4 19 | { x = 144, y = 48, width = 72, height = 48 }, # Sprite 5 20 | # Alien 3 21 | { x = 0, y = 96, width = 72, height = 48 }, # Sprite 6 22 | { x = 72, y = 96, width = 72, height = 48 }, # Sprite 7 23 | { x = 144, y = 96, width = 72, height = 48 }, # Sprite 8 24 | # Enemy bullet 25 | { x = 216, y = 0, width = 12, height = 48 }, # Sprite 9 26 | { x = 216, y = 48, width = 12, height = 48 }, # Sprite 10 27 | # Player bullet 28 | { x = 219, y = 96, width = 6, height = 48 }, # Sprite 11 29 | { x = 324, y = 126, width = 96, height = 18 }, # Sprite 12 30 | # Alien master 31 | { x = 228, y = 0, width = 96, height = 48 }, # Sprite 13 32 | { x = 228, y = 48, width = 96, height = 48 }, # Sprite 14 33 | { x = 228, y = 96, width = 96, height = 48 }, # Sprite 15 34 | # Player 35 | { x = 324, y = 0, width = 96, height = 48 }, # Sprite 16 36 | { x = 324, y = 48, width = 96, height = 48 }, # Sprite 17 37 | ] 38 | 39 | [sprite_sheet.game.animations.alien_loop_1] 40 | time = [0.0, 0.5, 1.0] 41 | sprite_number = [ 0, 1 ] 42 | 43 | [sprite_sheet.game.animations.alien_loop_2] 44 | time = [0.0, 0.5, 1.0] 45 | sprite_number = [ 3, 4 ] 46 | 47 | [sprite_sheet.game.animations.alien_loop_3] 48 | time = [0.0, 0.5, 1.0] 49 | sprite_number = [ 6, 7 ] 50 | 51 | [sprite_sheet.game.animations.alien_death_1] 52 | time = [0.0, 0.25] 53 | sprite_number = [ 2 ] 54 | 55 | [sprite_sheet.game.animations.alien_death_2] 56 | time = [0.0, 0.25] 57 | sprite_number = [ 5 ] 58 | 59 | [sprite_sheet.game.animations.alien_death_3] 60 | time = [0.0, 0.25] 61 | sprite_number = [ 8 ] 62 | 63 | [sprite_sheet.game.animations.enemy_bullet] 64 | time = [0.0, 0.35, 0.7] 65 | sprite_number = [ 9, 10 ] 66 | 67 | [sprite_sheet.game.animations.player_bullet_explosion] 68 | time = [0.0, 0.25] 69 | sprite_number = [ 12 ] 70 | 71 | [sprite_sheet.game.animations.alien_master_loop] 72 | time = [0.0, 0.1, 0.2] 73 | sprite_number = [ 13, 14 ] 74 | 75 | [sprite_sheet.game.animations.alien_master_death] 76 | time = [0.0, 0.25] 77 | sprite_number = [ 15 ] 78 | 79 | [sprite_sheet.game.animations.player_death] 80 | time = [0.0, 0.5] 81 | sprite_number = [ 17 ] 82 | -------------------------------------------------------------------------------- /lib/systems/move_alien.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | "math" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | ec "github.com/x-hgg-x/goecsengine/components" 11 | w "github.com/x-hgg-x/goecsengine/world" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | ) 15 | 16 | var moveAlienDirectionX = 1.0 17 | 18 | const ( 19 | moveAlienVelocity = 1000.0 / ebiten.DefaultTPS 20 | moveAlienDiffY = -24.0 21 | ) 22 | 23 | // MoveAlienSystem moves alien 24 | func MoveAlienSystem(world w.World) { 25 | gameComponents := world.Components.Game.(*gc.Components) 26 | gameEvents := &world.Resources.Game.(*resources.Game).Events 27 | 28 | alienSet := world.Manager.Join(gameComponents.Alien, gameComponents.AlienMaster.Not(), world.Components.Engine.Transform) 29 | if alienSet.Empty() { 30 | return 31 | } 32 | 33 | velocityRatio := float64(alienSet.Size()) 34 | 35 | minAlienPosX := float64(world.Resources.ScreenDimensions.Width) 36 | maxAlienPosX := 0.0 37 | minAlienPosY := float64(world.Resources.ScreenDimensions.Height) 38 | 39 | alienSet.Visit(ecs.Visit(func(entity ecs.Entity) { 40 | alien := gameComponents.Alien.Get(entity).(*gc.Alien) 41 | alienTranslation := world.Components.Engine.Transform.Get(entity).(*ec.Transform).Translation 42 | minAlienPosX = math.Min(minAlienPosX, alienTranslation.X-alien.Width/2) 43 | maxAlienPosX = math.Max(maxAlienPosX, alienTranslation.X+alien.Width/2) 44 | minAlienPosY = math.Min(minAlienPosY, alienTranslation.Y-alien.Height/2) 45 | })) 46 | 47 | var movementX, movementY float64 48 | if moveAlienDirectionX > 0 && maxAlienPosX < float64(world.Resources.ScreenDimensions.Width) { 49 | movementX = math.Min(moveAlienDirectionX*moveAlienVelocity/velocityRatio, float64(world.Resources.ScreenDimensions.Width)-maxAlienPosX) 50 | } else if moveAlienDirectionX < 0 && minAlienPosX > 0 { 51 | movementX = math.Max(moveAlienDirectionX*moveAlienVelocity/velocityRatio, -minAlienPosX) 52 | } else if moveAlienDirectionX > 0 && maxAlienPosX >= float64(world.Resources.ScreenDimensions.Width) || moveAlienDirectionX < 0 && minAlienPosX <= 0 { 53 | moveAlienDirectionX *= -1 54 | movementY = moveAlienDiffY 55 | 56 | // Lose a life when aliens reach the player line 57 | if playerLineEntity := ecs.GetFirst(world.Manager.Join(gameComponents.PlayerLine, world.Components.Engine.Transform)); playerLineEntity != nil { 58 | playerLineY := world.Components.Engine.Transform.Get(*playerLineEntity).(*ec.Transform).Translation.Y 59 | 60 | if minAlienPosY+moveAlienDiffY < playerLineY { 61 | gameEvents.LifeEvents = append(gameEvents.LifeEvents, resources.LifeEvent{}) 62 | gameEvents.ScoreEvents = append(gameEvents.ScoreEvents, resources.ScoreEvent{Score: -1000}) 63 | } 64 | } 65 | } 66 | 67 | alienSet.Visit(ecs.Visit(func(entity ecs.Entity) { 68 | alien := gameComponents.Alien.Get(entity).(*gc.Alien) 69 | alienTranslation := &world.Components.Engine.Transform.Get(entity).(*ec.Transform).Translation 70 | alienTranslation.X += movementX 71 | alienTranslation.Y += movementY 72 | alien.Translation.X += movementX 73 | alien.Translation.Y += movementY 74 | })) 75 | } 76 | -------------------------------------------------------------------------------- /lib/loader/bunker.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "image/color" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 8 | 9 | ecs "github.com/x-hgg-x/goecs/v2" 10 | ec "github.com/x-hgg-x/goecsengine/components" 11 | "github.com/x-hgg-x/goecsengine/loader" 12 | "github.com/x-hgg-x/goecsengine/math" 13 | "github.com/x-hgg-x/goecsengine/utils" 14 | w "github.com/x-hgg-x/goecsengine/world" 15 | 16 | "github.com/BurntSushi/toml" 17 | "github.com/hajimehoshi/ebiten/v2" 18 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 19 | ) 20 | 21 | // LoadBunkers creates pixel bunker entities for each bunker 22 | func LoadBunkers(world w.World) []ecs.Entity { 23 | gameComponents := world.Components.Game.(*gc.Components) 24 | 25 | // Get bunker image path 26 | type spriteSheetMetadata struct { 27 | SpriteSheets struct { 28 | Bunker struct { 29 | TextureImageName string `toml:"texture_image"` 30 | } 31 | } `toml:"sprite_sheet"` 32 | } 33 | 34 | var metadata spriteSheetMetadata 35 | utils.Try(toml.DecodeFile("assets/metadata/spritesheets/spritesheets.toml", &metadata)) 36 | 37 | // Load bunker image 38 | _, bunkerImage := utils.Try2(ebitenutil.NewImageFromFile(metadata.SpriteSheets.Bunker.TextureImageName)) 39 | 40 | // Load bunker entities 41 | bunkerEntities := loader.AddEntities(world, world.Resources.Prefabs.(*resources.Prefabs).Game.Bunker) 42 | if len(bunkerEntities) == 0 { 43 | return []ecs.Entity{} 44 | } 45 | 46 | // Create pixel image 47 | pixelSize := gameComponents.Bunker.Get(bunkerEntities[0]).(*gc.Bunker).PixelSize 48 | for _, bunkerEntity := range bunkerEntities { 49 | if pixelSize != gameComponents.Bunker.Get(bunkerEntity).(*gc.Bunker).PixelSize { 50 | utils.LogFatalf("pixel size must be the same for all bunkers") 51 | } 52 | } 53 | pixelImage := ebiten.NewImage(pixelSize, pixelSize) 54 | pixelImage.Fill(color.RGBA{0, 255, 0, 255}) 55 | 56 | // Create new bunker entities for each set of bunker pixels 57 | newBunkerEntities := []ecs.Entity{} 58 | for _, bunkerEntity := range bunkerEntities { 59 | bunkerSprite := world.Components.Engine.SpriteRender.Get(bunkerEntity).(*ec.SpriteRender) 60 | bunkerTransform := world.Components.Engine.Transform.Get(bunkerEntity).(*ec.Transform) 61 | 62 | bunkerSpriteWidth := float64(bunkerSprite.SpriteSheet.Sprites[bunkerSprite.SpriteNumber].Width) 63 | bunkerSpriteHeight := float64(bunkerSprite.SpriteSheet.Sprites[bunkerSprite.SpriteNumber].Height) 64 | 65 | bounds := bunkerImage.Bounds() 66 | for x := bounds.Min.X; x < bounds.Max.X; x += pixelSize { 67 | for y := bounds.Min.Y; y < bounds.Max.Y; y += pixelSize { 68 | if _, _, _, alpha := bunkerImage.At(x, y).RGBA(); alpha > 0 { 69 | newBunkerEntities = append(newBunkerEntities, world.Manager.NewEntity(). 70 | AddComponent(world.Components.Engine.SpriteRender, &ec.SpriteRender{ 71 | SpriteSheet: &ec.SpriteSheet{ 72 | Texture: ec.Texture{Image: pixelImage}, 73 | Sprites: []ec.Sprite{{X: 0, Y: 0, Width: pixelSize, Height: pixelSize}}, 74 | }, 75 | SpriteNumber: 0, 76 | }). 77 | AddComponent(world.Components.Engine.Transform, &ec.Transform{ 78 | Depth: bunkerTransform.Depth, 79 | Translation: math.Vector2{ 80 | X: bunkerTransform.Translation.X - bunkerSpriteWidth/2 + float64(x) + float64(pixelSize)/2, 81 | Y: bunkerTransform.Translation.Y + bunkerSpriteHeight/2 - float64(y) - float64(pixelSize)/2, 82 | }, 83 | }). 84 | AddComponent(gameComponents.Bunker, &gc.Bunker{PixelSize: pixelSize})) 85 | } 86 | } 87 | } 88 | // Delete old bunker entity 89 | world.Manager.DeleteEntity(bunkerEntity) 90 | } 91 | return newBunkerEntities 92 | } 93 | -------------------------------------------------------------------------------- /assets/metadata/entities/ui/highscores_menu.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | 3 | [entity.components.SpriteRender] 4 | fill = { width = 1000, height = 800, color = [0, 0, 0, 255] } 5 | 6 | [entity.components.Transform] 7 | translation = { x = 500.0, y = 400.0 } 8 | depth = 1.0 9 | 10 | 11 | [[entity]] 12 | 13 | [entity.components.Text] 14 | id = "score_difficulty" 15 | text = "NORMAL" 16 | font_face = { font = "joystix", options.size = 40.0 } 17 | color = [255, 255, 255, 255] 18 | 19 | [entity.components.UITransform] 20 | translation = { x = 500, y = 770 } 21 | 22 | 23 | [[entity]] 24 | 25 | [entity.components.Text] 26 | id = "score1" 27 | text = "1. AAAAAA 00000" 28 | font_face = { font = "joystix", options.size = 60.0 } 29 | color = [255, 255, 255, 0] 30 | 31 | [entity.components.UITransform] 32 | translation = { x = 500, y = 705 } 33 | pivot = "TopMiddle" 34 | 35 | 36 | [[entity]] 37 | 38 | [entity.components.Text] 39 | id = "score2" 40 | text = "2. AAAAAA 00000" 41 | font_face = { font = "joystix", options.size = 60.0 } 42 | color = [255, 255, 255, 0] 43 | 44 | [entity.components.UITransform] 45 | translation = { x = 500, y = 625 } 46 | pivot = "TopMiddle" 47 | 48 | 49 | [[entity]] 50 | 51 | [entity.components.Text] 52 | id = "score3" 53 | text = "3. AAAAAA 00000" 54 | font_face = { font = "joystix", options.size = 60.0 } 55 | color = [255, 255, 255, 0] 56 | 57 | [entity.components.UITransform] 58 | translation = { x = 500, y = 545 } 59 | pivot = "TopMiddle" 60 | 61 | 62 | [[entity]] 63 | 64 | [entity.components.Text] 65 | id = "score4" 66 | text = "4. AAAAAA 00000" 67 | font_face = { font = "joystix", options.size = 60.0 } 68 | color = [255, 255, 255, 0] 69 | 70 | [entity.components.UITransform] 71 | translation = { x = 500, y = 465 } 72 | pivot = "TopMiddle" 73 | 74 | 75 | [[entity]] 76 | 77 | [entity.components.Text] 78 | id = "score5" 79 | text = "5. AAAAAA 00000" 80 | font_face = { font = "joystix", options.size = 60.0 } 81 | color = [255, 255, 255, 0] 82 | 83 | [entity.components.UITransform] 84 | translation = { x = 500, y = 385 } 85 | pivot = "TopMiddle" 86 | 87 | 88 | [[entity]] 89 | 90 | [entity.components.Text] 91 | id = "score6" 92 | text = "6. AAAAAA 00000" 93 | font_face = { font = "joystix", options.size = 60.0 } 94 | color = [255, 255, 255, 0] 95 | 96 | [entity.components.UITransform] 97 | translation = { x = 500, y = 305 } 98 | pivot = "TopMiddle" 99 | 100 | 101 | [[entity]] 102 | 103 | [entity.components.Text] 104 | id = "score7" 105 | text = "7. AAAAAA 00000" 106 | font_face = { font = "joystix", options.size = 60.0 } 107 | color = [255, 255, 255, 0] 108 | 109 | [entity.components.UITransform] 110 | translation = { x = 500, y = 225 } 111 | pivot = "TopMiddle" 112 | 113 | 114 | [[entity]] 115 | 116 | [entity.components.Text] 117 | id = "score8" 118 | text = "8. AAAAAA 00000" 119 | font_face = { font = "joystix", options.size = 60.0 } 120 | color = [255, 255, 255, 0] 121 | 122 | [entity.components.UITransform] 123 | translation = { x = 500, y = 145 } 124 | pivot = "TopMiddle" 125 | 126 | 127 | [[entity]] 128 | 129 | [entity.components.Text] 130 | id = "score9" 131 | text = "9. AAAAAA 00000" 132 | font_face = { font = "joystix", options.size = 60.0 } 133 | color = [255, 255, 255, 0] 134 | 135 | [entity.components.UITransform] 136 | translation = { x = 500, y = 65 } 137 | pivot = "TopMiddle" 138 | 139 | 140 | [[entity]] 141 | 142 | [entity.components.Text] 143 | id = "arrow_left" 144 | text = "\u25c0" 145 | font_face = { font = "hack", options.size = 60.0 } 146 | color = [255, 255, 255, 0] 147 | 148 | [entity.components.UITransform] 149 | translation = { x = 50, y = 400 } 150 | 151 | 152 | [[entity]] 153 | 154 | [entity.components.Text] 155 | id = "arrow_right" 156 | text = "\u25b6" 157 | font_face = { font = "hack", options.size = 60.0 } 158 | color = [255, 255, 255, 0] 159 | 160 | [entity.components.UITransform] 161 | translation = { x = 950, y = 400 } 162 | -------------------------------------------------------------------------------- /lib/states/death.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 5 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 6 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 7 | 8 | ecs "github.com/x-hgg-x/goecs/v2" 9 | ec "github.com/x-hgg-x/goecsengine/components" 10 | "github.com/x-hgg-x/goecsengine/loader" 11 | "github.com/x-hgg-x/goecsengine/math" 12 | "github.com/x-hgg-x/goecsengine/states" 13 | w "github.com/x-hgg-x/goecsengine/world" 14 | 15 | "github.com/hajimehoshi/ebiten/v2" 16 | "github.com/hajimehoshi/ebiten/v2/inpututil" 17 | ) 18 | 19 | // DeathState is the player death state 20 | type DeathState struct { 21 | playerEntity ecs.Entity 22 | playerAnimation *ec.AnimationControl 23 | } 24 | 25 | // OnStart method 26 | func (st *DeathState) OnStart(world w.World) { 27 | // Restart player death animation 28 | gameComponents := world.Components.Game.(*gc.Components) 29 | world.Manager.Join(gameComponents.Player, gameComponents.Controllable, world.Components.Engine.AnimationControl).Visit(ecs.Visit(func(playerEntity ecs.Entity) { 30 | st.playerEntity = playerEntity 31 | st.playerAnimation = world.Components.Engine.AnimationControl.Get(playerEntity).(*ec.AnimationControl) 32 | st.playerAnimation.Command.Type = ec.AnimationCommandStart 33 | })) 34 | 35 | ebiten.SetCursorMode(ebiten.CursorModeHidden) 36 | } 37 | 38 | // OnPause method 39 | func (st *DeathState) OnPause(world w.World) { 40 | ebiten.SetCursorMode(ebiten.CursorModeVisible) 41 | } 42 | 43 | // OnResume method 44 | func (st *DeathState) OnResume(world w.World) { 45 | ebiten.SetCursorMode(ebiten.CursorModeHidden) 46 | } 47 | 48 | // OnStop method 49 | func (st *DeathState) OnStop(world w.World) { 50 | ebiten.SetCursorMode(ebiten.CursorModeVisible) 51 | } 52 | 53 | // Update method 54 | func (st *DeathState) Update(world w.World) states.Transition { 55 | g.SoundSystem(world) 56 | 57 | if st.playerAnimation.GetState().Type == ec.ControlStateDone { 58 | gameResources := world.Resources.Game.(*resources.Game) 59 | if gameResources.Lives <= 0 { 60 | return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&HighscoresState{ 61 | newScore: &highscore{difficulty: gameResources.Difficulty, score: gameResources.Score}, 62 | exitTransition: states.Transition{Type: states.TransSwitch, NewStates: []states.State{&GameOverState{difficulty: gameResources.Difficulty}}}, 63 | }}} 64 | } 65 | 66 | world.Manager.DeleteEntity(st.playerEntity) 67 | resurrectPlayer(world) 68 | return states.Transition{Type: states.TransPop} 69 | } 70 | 71 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 72 | return states.Transition{Type: states.TransPush, NewStates: []states.State{&PauseMenuState{}}} 73 | } 74 | 75 | return states.Transition{} 76 | } 77 | 78 | func resurrectPlayer(world w.World) { 79 | gameComponents := world.Components.Game.(*gc.Components) 80 | 81 | // Reset position of remaining aliens 82 | world.Manager.Join(gameComponents.Alien, gameComponents.AlienMaster.Not(), world.Components.Engine.Transform).Visit(ecs.Visit(func(alienEntity ecs.Entity) { 83 | alien := gameComponents.Alien.Get(alienEntity).(*gc.Alien) 84 | alienTranslation := &world.Components.Engine.Transform.Get(alienEntity).(*ec.Transform).Translation 85 | 86 | alienTranslation.X -= alien.Translation.X 87 | alienTranslation.Y -= alien.Translation.Y 88 | alien.Translation = math.Vector2{} 89 | })) 90 | 91 | // Clear bullets 92 | world.Manager.Join(gameComponents.Bullet).Visit(ecs.Visit(func(enemyBulletEntity ecs.Entity) { 93 | world.Manager.DeleteEntity(enemyBulletEntity) 94 | })) 95 | 96 | // Clear alien master 97 | world.Manager.Join(gameComponents.AlienMaster).Visit(ecs.Visit(func(enemyBulletEntity ecs.Entity) { 98 | world.Manager.DeleteEntity(enemyBulletEntity) 99 | })) 100 | 101 | // Resurrect player 102 | loader.AddEntities(world, world.Resources.Prefabs.(*resources.Prefabs).Game.Player) 103 | } 104 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "image/png" 5 | 6 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 7 | gloader "github.com/x-hgg-x/space-invaders-go/lib/loader" 8 | gr "github.com/x-hgg-x/space-invaders-go/lib/resources" 9 | gs "github.com/x-hgg-x/space-invaders-go/lib/states" 10 | 11 | "github.com/x-hgg-x/goecsengine/loader" 12 | er "github.com/x-hgg-x/goecsengine/resources" 13 | es "github.com/x-hgg-x/goecsengine/states" 14 | "github.com/x-hgg-x/goecsengine/utils" 15 | w "github.com/x-hgg-x/goecsengine/world" 16 | 17 | "github.com/hajimehoshi/ebiten/v2" 18 | ) 19 | 20 | const ( 21 | gameWidth = 1000 22 | gameHeight = 800 23 | ) 24 | 25 | type mainGame struct { 26 | world w.World 27 | stateMachine es.StateMachine 28 | } 29 | 30 | func (game *mainGame) Layout(outsideWidth, outsideHeight int) (int, int) { 31 | return gameWidth, gameHeight 32 | } 33 | 34 | func (game *mainGame) Update() error { 35 | game.stateMachine.Update(game.world) 36 | return nil 37 | } 38 | 39 | func (game *mainGame) Draw(screen *ebiten.Image) { 40 | game.stateMachine.Draw(game.world, screen) 41 | } 42 | 43 | func main() { 44 | world := w.InitWorld(&gc.Components{}) 45 | 46 | // Init screen dimensions 47 | world.Resources.ScreenDimensions = &er.ScreenDimensions{Width: gameWidth, Height: gameHeight} 48 | 49 | // Load controls 50 | axes := []string{gr.PlayerAxis} 51 | actions := []string{gr.ShootAction, gr.EnableDisableSoundAction} 52 | controls, inputHandler := loader.LoadControls("config/controls.toml", axes, actions) 53 | world.Resources.Controls = &controls 54 | world.Resources.InputHandler = &inputHandler 55 | 56 | // Load sprite sheets 57 | spriteSheets := loader.LoadSpriteSheets("assets/metadata/spritesheets/spritesheets.toml") 58 | world.Resources.SpriteSheets = &spriteSheets 59 | 60 | // Load fonts 61 | fonts := loader.LoadFonts("assets/metadata/fonts/fonts.toml") 62 | world.Resources.Fonts = &fonts 63 | 64 | // Load prefabs 65 | world.Resources.Prefabs = &gr.Prefabs{ 66 | Menu: gr.MenuPrefabs{ 67 | MuteMenu: gloader.PreloadEntities("assets/metadata/entities/ui/mute_menu.toml", world), 68 | MainMenu: gloader.PreloadEntities("assets/metadata/entities/ui/main_menu.toml", world), 69 | DifficultyMenu: gloader.PreloadEntities("assets/metadata/entities/ui/difficulty_menu.toml", world), 70 | PauseMenu: gloader.PreloadEntities("assets/metadata/entities/ui/pause_menu.toml", world), 71 | GameOverMenu: gloader.PreloadEntities("assets/metadata/entities/ui/game_over_menu.toml", world), 72 | LevelCompleteMenu: gloader.PreloadEntities("assets/metadata/entities/ui/level_complete_menu.toml", world), 73 | HighscoresMenu: gloader.PreloadEntities("assets/metadata/entities/ui/highscores_menu.toml", world), 74 | }, 75 | Game: gr.GamePrefabs{ 76 | Background: gloader.PreloadEntities("assets/metadata/entities/background.toml", world), 77 | Alien: gloader.PreloadEntities("assets/metadata/entities/alien.toml", world), 78 | Player: gloader.PreloadEntities("assets/metadata/entities/player.toml", world), 79 | PlayerLine: gloader.PreloadEntities("assets/metadata/entities/player_line.toml", world), 80 | Bunker: gloader.PreloadEntities("assets/metadata/entities/bunker.toml", world), 81 | AlienMaster: gloader.PreloadEntities("assets/metadata/entities/alien_master.toml", world), 82 | PlayerBullet: gloader.PreloadEntities("assets/metadata/entities/player_bullet.toml", world), 83 | EnemyBullet: gloader.PreloadEntities("assets/metadata/entities/enemy_bullet.toml", world), 84 | Score: gloader.PreloadEntities("assets/metadata/entities/ui/score.toml", world), 85 | Life: gloader.PreloadEntities("assets/metadata/entities/ui/life.toml", world), 86 | Difficulty: gloader.PreloadEntities("assets/metadata/entities/ui/difficulty.toml", world), 87 | }, 88 | } 89 | 90 | ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) 91 | ebiten.SetWindowSize(gameWidth, gameHeight) 92 | ebiten.SetWindowTitle("Space Invaders") 93 | 94 | utils.LogError(ebiten.RunGame(&mainGame{world, es.Init(&gs.MuteMenuState{}, world)})) 95 | } 96 | -------------------------------------------------------------------------------- /lib/states/gameplay.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | gloader "github.com/x-hgg-x/space-invaders-go/lib/loader" 9 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 10 | g "github.com/x-hgg-x/space-invaders-go/lib/systems" 11 | 12 | ecs "github.com/x-hgg-x/goecs/v2" 13 | ec "github.com/x-hgg-x/goecsengine/components" 14 | "github.com/x-hgg-x/goecsengine/loader" 15 | "github.com/x-hgg-x/goecsengine/states" 16 | "github.com/x-hgg-x/goecsengine/utils" 17 | w "github.com/x-hgg-x/goecsengine/world" 18 | 19 | "github.com/hajimehoshi/ebiten/v2" 20 | "github.com/hajimehoshi/ebiten/v2/inpututil" 21 | ) 22 | 23 | // GameplayState is the main game state 24 | type GameplayState struct { 25 | game *resources.Game 26 | runningAnimations []*ec.AnimationControl 27 | } 28 | 29 | // OnStart method 30 | func (st *GameplayState) OnStart(world w.World) { 31 | // Init rand seed 32 | rand.Seed(time.Now().UnixNano()) 33 | 34 | // Load game and ui entities 35 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 36 | loader.AddEntities(world, prefabs.Game.Background) 37 | loader.AddEntities(world, prefabs.Game.Alien) 38 | loader.AddEntities(world, prefabs.Game.Player) 39 | loader.AddEntities(world, prefabs.Game.PlayerLine) 40 | scoreEntity := loader.AddEntities(world, prefabs.Game.Score) 41 | lifeEntity := loader.AddEntities(world, prefabs.Game.Life) 42 | difficultyEntity := loader.AddEntities(world, prefabs.Game.Difficulty) 43 | 44 | // Load bunkers 45 | gloader.LoadBunkers(world) 46 | 47 | // Set game 48 | world.Resources.Game = st.game 49 | 50 | // Set score text 51 | for iEntity := range scoreEntity { 52 | world.Components.Engine.Text.Get(scoreEntity[iEntity]).(*ec.Text).Text = fmt.Sprintf("SCORE: %d", st.game.Score) 53 | } 54 | 55 | // Set life text 56 | for iEntity := range lifeEntity { 57 | world.Components.Engine.Text.Get(lifeEntity[iEntity]).(*ec.Text).Text = fmt.Sprintf("LIVES: %d", st.game.Lives) 58 | } 59 | 60 | // Set difficulty text 61 | var difficulty string 62 | switch st.game.Difficulty { 63 | case resources.DifficultyEasy: 64 | difficulty = "EASY" 65 | case resources.DifficultyNormal: 66 | difficulty = "NORMAL" 67 | case resources.DifficultyHard: 68 | difficulty = "HARD" 69 | default: 70 | utils.LogFatalf("unknown difficulty: %v", st.game.Difficulty) 71 | } 72 | for iEntity := range difficultyEntity { 73 | world.Components.Engine.Text.Get(difficultyEntity[iEntity]).(*ec.Text).Text = difficulty 74 | } 75 | 76 | ebiten.SetCursorMode(ebiten.CursorModeHidden) 77 | } 78 | 79 | // OnPause method 80 | func (st *GameplayState) OnPause(world w.World) { 81 | // Pause running animations 82 | st.runningAnimations = []*ec.AnimationControl{} 83 | world.Manager.Join(world.Components.Engine.AnimationControl).Visit(ecs.Visit(func(entity ecs.Entity) { 84 | animationControl := world.Components.Engine.AnimationControl.Get(entity).(*ec.AnimationControl) 85 | if animationControl.GetState().Type == ec.ControlStateRunning { 86 | animationControl.Command.Type = ec.AnimationCommandPause 87 | st.runningAnimations = append(st.runningAnimations, animationControl) 88 | } 89 | })) 90 | 91 | ebiten.SetCursorMode(ebiten.CursorModeVisible) 92 | } 93 | 94 | // OnResume method 95 | func (st *GameplayState) OnResume(world w.World) { 96 | // Resume running animations 97 | for _, animationControl := range st.runningAnimations { 98 | animationControl.Command.Type = ec.AnimationCommandStart 99 | } 100 | st.runningAnimations = []*ec.AnimationControl{} 101 | 102 | ebiten.SetCursorMode(ebiten.CursorModeHidden) 103 | } 104 | 105 | // OnStop method 106 | func (st *GameplayState) OnStop(world w.World) { 107 | world.Resources.Game = nil 108 | world.Manager.DeleteAllEntities() 109 | 110 | ebiten.SetCursorMode(ebiten.CursorModeVisible) 111 | } 112 | 113 | // Update method 114 | func (st *GameplayState) Update(world w.World) states.Transition { 115 | musicPlayer := (*world.Resources.AudioPlayers)["music"] 116 | if !musicPlayer.IsPlaying() { 117 | musicPlayer.Rewind() 118 | musicPlayer.Play() 119 | } 120 | 121 | g.SoundSystem(world) 122 | g.MovePlayerSystem(world) 123 | g.SpawnAlienMasterSystem(world) 124 | g.MoveAlienMasterSystem(world) 125 | g.MoveAlienSystem(world) 126 | g.ShootPlayerBulletSystem(world) 127 | g.ShootEnemyBulletSystem(world) 128 | g.MoveBulletSystem(world) 129 | g.CollisionSystem(world) 130 | g.LifeSystem(world) 131 | g.ScoreSystem(world) 132 | g.DeleteSystem(world) 133 | 134 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 135 | return states.Transition{Type: states.TransPush, NewStates: []states.State{&PauseMenuState{}}} 136 | } 137 | 138 | gameResources := world.Resources.Game.(*resources.Game) 139 | switch gameResources.StateEvent { 140 | case resources.StateEventDeath: 141 | gameResources.StateEvent = resources.StateEventNone 142 | return states.Transition{Type: states.TransPush, NewStates: []states.State{&DeathState{}}} 143 | case resources.StateEventLevelComplete: 144 | gameResources.StateEvent = resources.StateEventNone 145 | return states.Transition{Type: states.TransPush, NewStates: []states.State{&LevelCompleteState{game: gameResources}}} 146 | } 147 | 148 | return states.Transition{} 149 | } 150 | -------------------------------------------------------------------------------- /lib/states/highscores_menu.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "os" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/x-hgg-x/space-invaders-go/lib/math" 12 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 13 | 14 | ecs "github.com/x-hgg-x/goecs/v2" 15 | ec "github.com/x-hgg-x/goecsengine/components" 16 | "github.com/x-hgg-x/goecsengine/loader" 17 | "github.com/x-hgg-x/goecsengine/states" 18 | "github.com/x-hgg-x/goecsengine/utils" 19 | w "github.com/x-hgg-x/goecsengine/world" 20 | 21 | "github.com/BurntSushi/toml" 22 | "github.com/hajimehoshi/ebiten/v2" 23 | "github.com/hajimehoshi/ebiten/v2/inpututil" 24 | ) 25 | 26 | const ( 27 | highscoreNum = 9 28 | maxAuthorLen = 6 29 | ) 30 | 31 | var regexpForbiddenChars = regexp.MustCompile("[[:^alnum:]]") 32 | 33 | type highscore struct { 34 | difficulty resources.Difficulty 35 | score int 36 | author string 37 | position int 38 | highscore *resources.Score 39 | } 40 | 41 | // HighscoresState is the highscores state 42 | type HighscoresState struct { 43 | highscoresMenu []ecs.Entity 44 | difficulties []resources.Difficulty 45 | difficultySelection int 46 | highscores resources.Highscores 47 | newScore *highscore 48 | exitTransition states.Transition 49 | } 50 | 51 | // 52 | // State interface 53 | // 54 | 55 | // OnPause method 56 | func (st *HighscoresState) OnPause(world w.World) {} 57 | 58 | // OnResume method 59 | func (st *HighscoresState) OnResume(world w.World) {} 60 | 61 | // OnStart method 62 | func (st *HighscoresState) OnStart(world w.World) { 63 | prefabs := world.Resources.Prefabs.(*resources.Prefabs) 64 | st.highscoresMenu = append(st.highscoresMenu, loader.AddEntities(world, prefabs.Menu.HighscoresMenu)...) 65 | st.difficulties = []resources.Difficulty{resources.DifficultyEasy, resources.DifficultyNormal, resources.DifficultyHard} 66 | 67 | // Load highscores 68 | toml.DecodeFile("config/highscores.toml", &st.highscores) 69 | normalizeHighScores(&st.highscores.Easy) 70 | normalizeHighScores(&st.highscores.Normal) 71 | normalizeHighScores(&st.highscores.Hard) 72 | 73 | if st.newScore != nil { 74 | st.difficultySelection = find(st.difficulties, st.newScore.difficulty) 75 | } else { 76 | st.difficultySelection = 1 77 | } 78 | 79 | // Display highscores and check if a new highscore has been made 80 | if newHighscore := st.displayHighScores(world); !newHighscore { 81 | st.newScore = nil 82 | } 83 | 84 | // Hide game ui 85 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 86 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 87 | if text.ID == "game_score" || text.ID == "game_life" || text.ID == "game_difficulty" { 88 | text.Color.A = 0 89 | } 90 | })) 91 | } 92 | 93 | // OnStop method 94 | func (st *HighscoresState) OnStop(world w.World) { 95 | // Show game ui 96 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 97 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 98 | if text.ID == "game_score" || text.ID == "game_life" || text.ID == "game_difficulty" { 99 | text.Color.A = 255 100 | } 101 | })) 102 | 103 | world.Manager.DeleteEntities(st.highscoresMenu...) 104 | } 105 | 106 | // Update method 107 | func (st *HighscoresState) Update(world w.World) states.Transition { 108 | if st.newScore != nil { 109 | // Set highscore author 110 | // Get user input 111 | st.newScore.author += strings.ToUpper(regexpForbiddenChars.ReplaceAllLiteralString(string(ebiten.AppendInputChars(nil)), "")) 112 | if len(st.newScore.author) > maxAuthorLen { 113 | st.newScore.author = st.newScore.author[:maxAuthorLen] 114 | } 115 | if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(st.newScore.author) > 0 { 116 | st.newScore.author = st.newScore.author[:len(st.newScore.author)-1] 117 | } 118 | 119 | // Set new score text 120 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 121 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 122 | if text.ID == fmt.Sprintf("score%d", st.newScore.position+1) { 123 | padding := strings.Repeat("_", maxAuthorLen-len(st.newScore.author)) 124 | text.Text = fmt.Sprintf("%d. %s%s %5d", st.newScore.position+1, st.newScore.author, padding, st.newScore.score) 125 | } 126 | })) 127 | 128 | // Validate score 129 | if inpututil.IsKeyJustPressed(ebiten.KeyEnter) { 130 | st.newScore.highscore.Author = st.newScore.author 131 | st.newScore = nil 132 | 133 | // Save highscores 134 | var encoded strings.Builder 135 | encoder := toml.NewEncoder(&encoded) 136 | encoder.Indent = "" 137 | utils.LogError(encoder.Encode(st.highscores)) 138 | utils.LogError(os.WriteFile("config/highscores.toml", []byte(encoded.String()), 0o666)) 139 | 140 | st.displayHighScores(world) 141 | } 142 | } else { 143 | // View all scores by looping difficulties 144 | if inpututil.IsKeyJustPressed(ebiten.KeyLeft) { 145 | st.difficultySelection = math.Mod(st.difficultySelection-1, len(st.difficulties)) 146 | st.displayHighScores(world) 147 | } 148 | 149 | if inpututil.IsKeyJustPressed(ebiten.KeyRight) { 150 | st.difficultySelection = math.Mod(st.difficultySelection+1, len(st.difficulties)) 151 | st.displayHighScores(world) 152 | } 153 | 154 | // Exit 155 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) { 156 | return st.exitTransition 157 | } 158 | } 159 | return states.Transition{} 160 | } 161 | 162 | func (st *HighscoresState) displayHighScores(world w.World) bool { 163 | var difficultyText string 164 | var scores *[]resources.Score 165 | switch st.difficulties[st.difficultySelection] { 166 | case resources.DifficultyEasy: 167 | difficultyText = "EASY" 168 | scores = &st.highscores.Easy.Scores 169 | case resources.DifficultyNormal: 170 | difficultyText = "NORMAL" 171 | scores = &st.highscores.Normal.Scores 172 | case resources.DifficultyHard: 173 | difficultyText = "HARD" 174 | scores = &st.highscores.Hard.Scores 175 | default: 176 | utils.LogFatalf("unknown difficulty: %v", st.difficulties[st.difficultySelection]) 177 | } 178 | 179 | // Sort scores 180 | sort.SliceStable(*scores, func(i, j int) bool { 181 | return (*scores)[i].Score > (*scores)[j].Score 182 | }) 183 | 184 | // Get new score position 185 | newHighscore := false 186 | if st.newScore != nil { 187 | position := 0 188 | for _, score := range *scores { 189 | if st.newScore.score <= score.Score { 190 | position++ 191 | } 192 | } 193 | 194 | if position < highscoreNum { 195 | (*scores) = append((*scores), resources.Score{}) 196 | copy((*scores)[position+1:], (*scores)[position:]) 197 | (*scores)[position] = resources.Score{Score: st.newScore.score} 198 | st.newScore.position = position 199 | st.newScore.highscore = &(*scores)[position] 200 | newHighscore = true 201 | 202 | if len(*scores) > highscoreNum { 203 | *scores = (*scores)[:highscoreNum] 204 | } 205 | } 206 | } 207 | 208 | // Set score texts 209 | for iScore := 0; iScore < highscoreNum; iScore++ { 210 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 211 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 212 | if text.ID == fmt.Sprintf("score%d", iScore+1) { 213 | text.Color.A = 0 214 | if iScore < len((*scores)) { 215 | text.Text = fmt.Sprintf("%d. %-6s %5d", iScore+1, (*scores)[iScore].Author, (*scores)[iScore].Score) 216 | text.Color = color.RGBA{R: 255, G: 255, B: 255, A: 255} 217 | if newHighscore && iScore == st.newScore.position { 218 | text.Color = color.RGBA{R: 255, A: 255} 219 | } 220 | } 221 | } 222 | })) 223 | } 224 | 225 | // Set other texts 226 | world.Manager.Join(world.Components.Engine.Text, world.Components.Engine.UITransform).Visit(ecs.Visit(func(entity ecs.Entity) { 227 | text := world.Components.Engine.Text.Get(entity).(*ec.Text) 228 | if text.ID == "score_difficulty" { 229 | text.Text = difficultyText 230 | } 231 | if !newHighscore && (text.ID == "arrow_left" || text.ID == "arrow_right") { 232 | text.Color.A = 255 233 | } 234 | })) 235 | 236 | return newHighscore 237 | } 238 | 239 | func normalizeHighScores(t *resources.ScoreTable) { 240 | if len(t.Scores) > highscoreNum { 241 | t.Scores = t.Scores[:highscoreNum] 242 | } 243 | 244 | for i := range t.Scores { 245 | t.Scores[i].Author = strings.ToUpper(regexpForbiddenChars.ReplaceAllLiteralString(t.Scores[i].Author, "")) 246 | if len(t.Scores[i].Author) > maxAuthorLen { 247 | t.Scores[i].Author = t.Scores[i].Author[:maxAuthorLen] 248 | } 249 | } 250 | } 251 | 252 | func find(slice []resources.Difficulty, x resources.Difficulty) int { 253 | for i, e := range slice { 254 | if x == e { 255 | return i 256 | } 257 | } 258 | return len(slice) 259 | } 260 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 4 | github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 h1:A8UnJ/5OKzki4HBDwoRQz7I6sxKsokpMXcGh+fUxpfc= 5 | github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= 6 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad h1:kX51IjbsJPCvzV9jUoVQG9GEUqIq5hjfYzXTqQ52Rh8= 7 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 10 | github.com/hajimehoshi/bitmapfont/v2 v2.2.2 h1:4z08Fk1m3pjtlO7BdoP48u5bp/Y8xmKshf44aCXgYpE= 11 | github.com/hajimehoshi/bitmapfont/v2 v2.2.2/go.mod h1:Ua/x9Dkz7M9CU4zr1VHWOqGwjKdXbOTRsH7lWfb1Co0= 12 | github.com/hajimehoshi/ebiten/v2 v2.4.12 h1:exd4SRImAKJkoRGV3nlYUeFGmM6U/rVD3vWlgnO2mUo= 13 | github.com/hajimehoshi/ebiten/v2 v2.4.12/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4= 14 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 h1:s01qIIRG7vN/5ndLwkDktjx44ulFk6apvAjVBYR50Yo= 15 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 16 | github.com/hajimehoshi/go-mp3 v0.3.3 h1:cWnfRdpye2m9ElSoVqneYRcpt/l3ijttgjMeQh+r+FE= 17 | github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 18 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 19 | github.com/hajimehoshi/oto/v2 v2.3.1 h1:qrLKpNus2UfD674oxckKjNJmesp9hMh7u7QCrStB3Rc= 20 | github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= 21 | github.com/jakecoffman/cp v1.2.1/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 22 | github.com/jezek/xgb v1.0.1 h1:YUGhxps0aR7J2Xplbs23OHnV1mWaxFVcOl9b+1RQkt8= 23 | github.com/jezek/xgb v1.0.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 24 | github.com/jfreymuth/oggvorbis v1.0.4 h1:cyJCd0XSoxkKzUPmqM0ZoQJ0h/WbhfyvUR+FTMxQEac= 25 | github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= 26 | github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE= 27 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= 28 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 29 | github.com/x-hgg-x/goecs/v2 v2.0.5 h1:Tn1Na7YcQRMODfQn2zCLCbpkPkCiSmZo39WPv1Az0z4= 30 | github.com/x-hgg-x/goecs/v2 v2.0.5/go.mod h1:v7U7OcJneN0L/nxpqitx/csEPRjzbA9i5m3l3WFmkHg= 31 | github.com/x-hgg-x/goecsengine v0.11.2 h1:UTYlde5d7SDlcvpvvuQXsvg2ZDt45mvSKvrFckI/l4g= 32 | github.com/x-hgg-x/goecsengine v0.11.2/go.mod h1:88cpIpXi2ZDVsZyPcMM3KQIl7oLABTi+EFnmGEAPBQE= 33 | github.com/yourbasic/bit v0.0.0-20180313074424-45a4409f4082 h1:AWIZQ6fJPAAZdCUElj007LvHa/ER8nOn3CHWajn+1QY= 34 | github.com/yourbasic/bit v0.0.0-20180313074424-45a4409f4082/go.mod h1:SC4yTthuwUIud4hT6D7kJGIYmhnskaQnm3VD2VYM8EM= 35 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 36 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 38 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 39 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 40 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 41 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 42 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 43 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 44 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 45 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 46 | golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= 47 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 48 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 49 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 50 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 51 | golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk= 52 | golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= 53 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 54 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 55 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 56 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 57 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 58 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 59 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 60 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 61 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 79 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 81 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 86 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 87 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 88 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 89 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 90 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 91 | golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 92 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 93 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 95 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 96 | -------------------------------------------------------------------------------- /lib/systems/collision.go: -------------------------------------------------------------------------------- 1 | package systems 2 | 3 | import ( 4 | gc "github.com/x-hgg-x/space-invaders-go/lib/components" 5 | "github.com/x-hgg-x/space-invaders-go/lib/resources" 6 | 7 | ecs "github.com/x-hgg-x/goecs/v2" 8 | ec "github.com/x-hgg-x/goecsengine/components" 9 | "github.com/x-hgg-x/goecsengine/utils" 10 | w "github.com/x-hgg-x/goecsengine/world" 11 | ) 12 | 13 | // CollisionSystem manages collisions 14 | func CollisionSystem(world w.World) { 15 | gameComponents := world.Components.Game.(*gc.Components) 16 | gameResources := world.Resources.Game.(*resources.Game) 17 | gameEvents := &gameResources.Events 18 | audioPlayers := *world.Resources.AudioPlayers 19 | 20 | screenHeight := float64(world.Resources.ScreenDimensions.Height) 21 | 22 | // Player bullet explosion at the top of the screen 23 | world.Manager.Join(gameComponents.Player, gameComponents.Bullet, world.Components.Engine.SpriteRender, world.Components.Engine.Transform).Visit(ecs.Visit(func(playerBulletEntity ecs.Entity) { 24 | playerBullet := gameComponents.Bullet.Get(playerBulletEntity).(*gc.Bullet) 25 | playerBulletSprite := world.Components.Engine.SpriteRender.Get(playerBulletEntity).(*ec.SpriteRender) 26 | playerBulletTranslation := &world.Components.Engine.Transform.Get(playerBulletEntity).(*ec.Transform).Translation 27 | 28 | if playerBulletTranslation.Y >= screenHeight-playerBullet.Height/2 { 29 | animation := playerBulletSprite.SpriteSheet.Animations[resources.PlayerBulletExplosionAnimation] 30 | firstSprite := playerBulletSprite.SpriteSheet.Sprites[animation.SpriteNumber[0]] 31 | 32 | playerBulletTranslation.Y = screenHeight - float64(firstSprite.Height)/2 33 | 34 | playerBulletEntity. 35 | RemoveComponent(gameComponents.Bullet). 36 | AddComponent(gameComponents.Deleted, &gc.Deleted{}). 37 | AddComponent(world.Components.Engine.AnimationControl, &ec.AnimationControl{ 38 | Animation: animation, 39 | Command: ec.AnimationCommand{Type: ec.AnimationCommandStart}, 40 | RateMultiplier: 1, 41 | }) 42 | } 43 | })) 44 | 45 | // Remove enemy bullet at the bottom of the screen 46 | world.Manager.Join(gameComponents.Enemy, gameComponents.Bullet, world.Components.Engine.Transform).Visit(ecs.Visit(func(enemyBulletEntity ecs.Entity) { 47 | enemyBullet := gameComponents.Bullet.Get(enemyBulletEntity).(*gc.Bullet) 48 | enemyBulletTranslation := &world.Components.Engine.Transform.Get(enemyBulletEntity).(*ec.Transform).Translation 49 | 50 | if enemyBulletTranslation.Y <= -enemyBullet.Height/2 { 51 | world.Manager.DeleteEntity(enemyBulletEntity) 52 | } 53 | })) 54 | 55 | // Collision between player bullets and aliens 56 | world.Manager.Join(gameComponents.Player, gameComponents.Bullet, world.Components.Engine.Transform).Visit(ecs.Visit(func(playerBulletEntity ecs.Entity) { 57 | playerBullet := gameComponents.Bullet.Get(playerBulletEntity).(*gc.Bullet) 58 | playerBulletTranslation := world.Components.Engine.Transform.Get(playerBulletEntity).(*ec.Transform).Translation 59 | 60 | world.Manager.Join(gameComponents.Alien, world.Components.Engine.AnimationControl).Visit( 61 | func(index int) (skip bool) { 62 | alienEntity := ecs.Entity(index) 63 | alien := gameComponents.Alien.Get(alienEntity).(*gc.Alien) 64 | alienSprite := world.Components.Engine.SpriteRender.Get(alienEntity).(*ec.SpriteRender) 65 | alienTranslation := world.Components.Engine.Transform.Get(alienEntity).(*ec.Transform).Translation 66 | alienAnimationControl := world.Components.Engine.AnimationControl.Get(alienEntity).(*ec.AnimationControl) 67 | 68 | if !rectangleCollision(alienTranslation.X, alienTranslation.Y, alien.Width, alien.Height, playerBulletTranslation.X, playerBulletTranslation.Y, playerBullet.Width, playerBullet.Height) { 69 | // Check next alien 70 | return false 71 | } 72 | 73 | // Only one alien is killed for each bullet 74 | world.Manager.DeleteEntity(playerBulletEntity) 75 | 76 | var newAlienAnimation *ec.Animation 77 | for key := range alienSprite.SpriteSheet.Animations { 78 | if alienSprite.SpriteSheet.Animations[key] == alienAnimationControl.Animation { 79 | switch key { 80 | case resources.AlienLoop1Animation: 81 | newAlienAnimation = alienSprite.SpriteSheet.Animations[resources.AlienDeath1Animation] 82 | gameEvents.ScoreEvents = append(gameEvents.ScoreEvents, resources.ScoreEvent{Score: 100}) 83 | case resources.AlienLoop2Animation: 84 | newAlienAnimation = alienSprite.SpriteSheet.Animations[resources.AlienDeath2Animation] 85 | gameEvents.ScoreEvents = append(gameEvents.ScoreEvents, resources.ScoreEvent{Score: 200}) 86 | case resources.AlienLoop3Animation: 87 | newAlienAnimation = alienSprite.SpriteSheet.Animations[resources.AlienDeath3Animation] 88 | gameEvents.ScoreEvents = append(gameEvents.ScoreEvents, resources.ScoreEvent{Score: 300}) 89 | case resources.AlienMasterLoopAnimation: 90 | newAlienAnimation = alienSprite.SpriteSheet.Animations[resources.AlienMasterDeathAnimation] 91 | gameEvents.ScoreEvents = append(gameEvents.ScoreEvents, resources.ScoreEvent{Score: 1000}) 92 | default: 93 | utils.LogFatalf("unknown animation name: '%s'", key) 94 | } 95 | break 96 | } 97 | } 98 | if newAlienAnimation == nil { 99 | utils.LogFatalf("unable to find animation") 100 | } 101 | 102 | *alienAnimationControl = ec.AnimationControl{ 103 | Animation: newAlienAnimation, 104 | Command: ec.AnimationCommand{Type: ec.AnimationCommandStart}, 105 | RateMultiplier: 1, 106 | } 107 | alienEntity.RemoveComponent(gameComponents.Alien).AddComponent(gameComponents.Deleted, &gc.Deleted{}) 108 | 109 | audioPlayers["killed"].Rewind() 110 | audioPlayers["killed"].Play() 111 | 112 | // Skip other aliens 113 | return true 114 | }) 115 | })) 116 | 117 | // Collision between player bullets and enemy bullets 118 | world.Manager.Join(gameComponents.Player, gameComponents.Bullet, world.Components.Engine.Transform).Visit(ecs.Visit(func(playerBulletEntity ecs.Entity) { 119 | playerBullet := gameComponents.Bullet.Get(playerBulletEntity).(*gc.Bullet) 120 | playerBulletTranslation := world.Components.Engine.Transform.Get(playerBulletEntity).(*ec.Transform).Translation 121 | 122 | world.Manager.Join(gameComponents.Enemy, gameComponents.Bullet, world.Components.Engine.Transform).Visit(ecs.Visit(func(enemyBulletEntity ecs.Entity) { 123 | enemyBullet := gameComponents.Bullet.Get(enemyBulletEntity).(*gc.Bullet) 124 | enemyBulletTranslation := world.Components.Engine.Transform.Get(enemyBulletEntity).(*ec.Transform).Translation 125 | 126 | if rectangleCollision(enemyBulletTranslation.X, enemyBulletTranslation.Y, enemyBullet.Width, enemyBullet.Height, playerBulletTranslation.X, playerBulletTranslation.Y, playerBullet.Width, playerBullet.Height) { 127 | world.Manager.DeleteEntity(playerBulletEntity) 128 | world.Manager.DeleteEntity(enemyBulletEntity) 129 | } 130 | })) 131 | })) 132 | 133 | // Collision between bullets and bunkers 134 | world.Manager.Join(gameComponents.Bullet, world.Components.Engine.Transform).Visit(ecs.Visit(func(bulletEntity ecs.Entity) { 135 | bullet := gameComponents.Bullet.Get(bulletEntity).(*gc.Bullet) 136 | bulletTranslation := world.Components.Engine.Transform.Get(bulletEntity).(*ec.Transform).Translation 137 | 138 | world.Manager.Join(gameComponents.Bunker, world.Components.Engine.Transform).Visit(ecs.Visit(func(bunkerEntity ecs.Entity) { 139 | bunkerPixelSize := float64(gameComponents.Bunker.Get(bunkerEntity).(*gc.Bunker).PixelSize) 140 | bunkerTranslation := world.Components.Engine.Transform.Get(bunkerEntity).(*ec.Transform).Translation 141 | 142 | if rectangleCollision(bunkerTranslation.X, bunkerTranslation.Y, bunkerPixelSize, bunkerPixelSize, bulletTranslation.X, bulletTranslation.Y, bullet.Width, bullet.Height) { 143 | world.Manager.DeleteEntity(bunkerEntity) 144 | bullet.Health -= bunkerPixelSize * bunkerPixelSize 145 | } 146 | })) 147 | 148 | if bullet.Health <= 0 { 149 | world.Manager.DeleteEntity(bulletEntity) 150 | } 151 | })) 152 | 153 | // Collision between player and enemy bullets 154 | world.Manager.Join(gameComponents.Player, gameComponents.Controllable, world.Components.Engine.SpriteRender, world.Components.Engine.Transform).Visit(ecs.Visit(func(playerEntity ecs.Entity) { 155 | playerControllable := gameComponents.Controllable.Get(playerEntity).(*gc.Controllable) 156 | playerTranslation := world.Components.Engine.Transform.Get(playerEntity).(*ec.Transform).Translation 157 | 158 | world.Manager.Join(gameComponents.Enemy, gameComponents.Bullet, world.Components.Engine.Transform).Visit( 159 | func(index int) (skip bool) { 160 | enemyBulletEntity := ecs.Entity(index) 161 | enemyBullet := gameComponents.Bullet.Get(enemyBulletEntity).(*gc.Bullet) 162 | enemyBulletTranslation := world.Components.Engine.Transform.Get(enemyBulletEntity).(*ec.Transform).Translation 163 | 164 | if !rectangleCollision(playerTranslation.X, playerTranslation.Y, playerControllable.Width, playerControllable.Height, enemyBulletTranslation.X, enemyBulletTranslation.Y, enemyBullet.Width, enemyBullet.Height) { 165 | return false 166 | } 167 | 168 | world.Manager.DeleteEntity(enemyBulletEntity) 169 | gameEvents.LifeEvents = append(gameEvents.LifeEvents, resources.LifeEvent{}) 170 | gameEvents.ScoreEvents = append(gameEvents.ScoreEvents, resources.ScoreEvent{Score: -1000}) 171 | 172 | audioPlayers["explosion"].Rewind() 173 | audioPlayers["explosion"].Play() 174 | return true 175 | }) 176 | })) 177 | 178 | // Finish level if no alien are left 179 | if world.Manager.Join(gameComponents.Alien, gameComponents.AlienMaster.Not()).Empty() { 180 | gameResources.StateEvent = resources.StateEventLevelComplete 181 | } 182 | } 183 | 184 | func rectangleCollision(r1X, r1Y, r1Width, r1Height, r2X, r2Y, r2Width, r2Height float64) bool { 185 | return r1X-r1Width/2-r2Width/2 <= r2X && r2X <= r1X+r1Width/2+r2Width/2 && r1Y-r1Height/2-r2Height/2 <= r2Y && r2Y <= r1Y+r1Height/2+r2Height/2 186 | } 187 | -------------------------------------------------------------------------------- /assets/metadata/entities/alien.toml: -------------------------------------------------------------------------------- 1 | [[entity]] 2 | [entity.components] 3 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 4 | Transform.translation = { x = 95.0, y = 400.0 } 5 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 6 | Enemy = {} 7 | Alien = { width = 72.0, height = 48.0 } 8 | 9 | [[entity]] 10 | [entity.components] 11 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 12 | Transform.translation = { x = 185.0, y = 400.0 } 13 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 14 | Enemy = {} 15 | Alien = { width = 72.0, height = 48.0 } 16 | 17 | [[entity]] 18 | [entity.components] 19 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 20 | Transform.translation = { x = 275.0, y = 400.0 } 21 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 22 | Enemy = {} 23 | Alien = { width = 72.0, height = 48.0 } 24 | 25 | [[entity]] 26 | [entity.components] 27 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 28 | Transform.translation = { x = 365.0, y = 400.0 } 29 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 30 | Enemy = {} 31 | Alien = { width = 72.0, height = 48.0 } 32 | 33 | [[entity]] 34 | [entity.components] 35 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 36 | Transform.translation = { x = 455.0, y = 400.0 } 37 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 38 | Enemy = {} 39 | Alien = { width = 72.0, height = 48.0 } 40 | 41 | [[entity]] 42 | [entity.components] 43 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 44 | Transform.translation = { x = 545.0, y = 400.0 } 45 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 46 | Enemy = {} 47 | Alien = { width = 72.0, height = 48.0 } 48 | 49 | [[entity]] 50 | [entity.components] 51 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 52 | Transform.translation = { x = 635.0, y = 400.0 } 53 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 54 | Enemy = {} 55 | Alien = { width = 72.0, height = 48.0 } 56 | 57 | [[entity]] 58 | [entity.components] 59 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 60 | Transform.translation = { x = 725.0, y = 400.0 } 61 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 62 | Enemy = {} 63 | Alien = { width = 72.0, height = 48.0 } 64 | 65 | [[entity]] 66 | [entity.components] 67 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 68 | Transform.translation = { x = 815.0, y = 400.0 } 69 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 70 | Enemy = {} 71 | Alien = { width = 72.0, height = 48.0 } 72 | 73 | [[entity]] 74 | [entity.components] 75 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 76 | Transform.translation = { x = 905.0, y = 400.0 } 77 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 78 | Enemy = {} 79 | Alien = { width = 72.0, height = 48.0 } 80 | 81 | 82 | [[entity]] 83 | [entity.components] 84 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 85 | Transform.translation = { x = 95.0, y = 472.0 } 86 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 87 | Enemy = {} 88 | Alien = { width = 72.0, height = 48.0 } 89 | 90 | [[entity]] 91 | [entity.components] 92 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 93 | Transform.translation = { x = 185.0, y = 472.0 } 94 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 95 | Enemy = {} 96 | Alien = { width = 72.0, height = 48.0 } 97 | 98 | [[entity]] 99 | [entity.components] 100 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 101 | Transform.translation = { x = 275.0, y = 472.0 } 102 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 103 | Enemy = {} 104 | Alien = { width = 72.0, height = 48.0 } 105 | 106 | [[entity]] 107 | [entity.components] 108 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 109 | Transform.translation = { x = 365.0, y = 472.0 } 110 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 111 | Enemy = {} 112 | Alien = { width = 72.0, height = 48.0 } 113 | 114 | [[entity]] 115 | [entity.components] 116 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 117 | Transform.translation = { x = 455.0, y = 472.0 } 118 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 119 | Enemy = {} 120 | Alien = { width = 72.0, height = 48.0 } 121 | 122 | [[entity]] 123 | [entity.components] 124 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 125 | Transform.translation = { x = 545.0, y = 472.0 } 126 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 127 | Enemy = {} 128 | Alien = { width = 72.0, height = 48.0 } 129 | 130 | [[entity]] 131 | [entity.components] 132 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 133 | Transform.translation = { x = 635.0, y = 472.0 } 134 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 135 | Enemy = {} 136 | Alien = { width = 72.0, height = 48.0 } 137 | 138 | [[entity]] 139 | [entity.components] 140 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 141 | Transform.translation = { x = 725.0, y = 472.0 } 142 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 143 | Enemy = {} 144 | Alien = { width = 72.0, height = 48.0 } 145 | 146 | [[entity]] 147 | [entity.components] 148 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 149 | Transform.translation = { x = 815.0, y = 472.0 } 150 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 151 | Enemy = {} 152 | Alien = { width = 72.0, height = 48.0 } 153 | 154 | [[entity]] 155 | [entity.components] 156 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 0 } 157 | Transform.translation = { x = 905.0, y = 472.0 } 158 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_1", end.type = "Loop" } 159 | Enemy = {} 160 | Alien = { width = 72.0, height = 48.0 } 161 | 162 | 163 | [[entity]] 164 | [entity.components] 165 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 166 | Transform.translation = { x = 95.0, y = 544.0 } 167 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 168 | Enemy = {} 169 | Alien = { width = 66.0, height = 48.0 } 170 | 171 | [[entity]] 172 | [entity.components] 173 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 174 | Transform.translation = { x = 185.0, y = 544.0 } 175 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 176 | Enemy = {} 177 | Alien = { width = 66.0, height = 48.0 } 178 | 179 | [[entity]] 180 | [entity.components] 181 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 182 | Transform.translation = { x = 275.0, y = 544.0 } 183 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 184 | Enemy = {} 185 | Alien = { width = 66.0, height = 48.0 } 186 | 187 | [[entity]] 188 | [entity.components] 189 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 190 | Transform.translation = { x = 365.0, y = 544.0 } 191 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 192 | Enemy = {} 193 | Alien = { width = 66.0, height = 48.0 } 194 | 195 | [[entity]] 196 | [entity.components] 197 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 198 | Transform.translation = { x = 455.0, y = 544.0 } 199 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 200 | Enemy = {} 201 | Alien = { width = 66.0, height = 48.0 } 202 | 203 | [[entity]] 204 | [entity.components] 205 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 206 | Transform.translation = { x = 545.0, y = 544.0 } 207 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 208 | Enemy = {} 209 | Alien = { width = 66.0, height = 48.0 } 210 | 211 | [[entity]] 212 | [entity.components] 213 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 214 | Transform.translation = { x = 635.0, y = 544.0 } 215 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 216 | Enemy = {} 217 | Alien = { width = 66.0, height = 48.0 } 218 | 219 | [[entity]] 220 | [entity.components] 221 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 222 | Transform.translation = { x = 725.0, y = 544.0 } 223 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 224 | Enemy = {} 225 | Alien = { width = 66.0, height = 48.0 } 226 | 227 | [[entity]] 228 | [entity.components] 229 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 230 | Transform.translation = { x = 815.0, y = 544.0 } 231 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 232 | Enemy = {} 233 | Alien = { width = 66.0, height = 48.0 } 234 | 235 | [[entity]] 236 | [entity.components] 237 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 238 | Transform.translation = { x = 905.0, y = 544.0 } 239 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 240 | Enemy = {} 241 | Alien = { width = 66.0, height = 48.0 } 242 | 243 | 244 | [[entity]] 245 | [entity.components] 246 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 247 | Transform.translation = { x = 95.0, y = 616.0 } 248 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 249 | Enemy = {} 250 | Alien = { width = 66.0, height = 48.0 } 251 | 252 | [[entity]] 253 | [entity.components] 254 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 255 | Transform.translation = { x = 185.0, y = 616.0 } 256 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 257 | Enemy = {} 258 | Alien = { width = 66.0, height = 48.0 } 259 | 260 | [[entity]] 261 | [entity.components] 262 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 263 | Transform.translation = { x = 275.0, y = 616.0 } 264 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 265 | Enemy = {} 266 | Alien = { width = 66.0, height = 48.0 } 267 | 268 | [[entity]] 269 | [entity.components] 270 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 271 | Transform.translation = { x = 365.0, y = 616.0 } 272 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 273 | Enemy = {} 274 | Alien = { width = 66.0, height = 48.0 } 275 | 276 | [[entity]] 277 | [entity.components] 278 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 279 | Transform.translation = { x = 455.0, y = 616.0 } 280 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 281 | Enemy = {} 282 | Alien = { width = 66.0, height = 48.0 } 283 | 284 | [[entity]] 285 | [entity.components] 286 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 287 | Transform.translation = { x = 545.0, y = 616.0 } 288 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 289 | Enemy = {} 290 | Alien = { width = 66.0, height = 48.0 } 291 | 292 | [[entity]] 293 | [entity.components] 294 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 295 | Transform.translation = { x = 635.0, y = 616.0 } 296 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 297 | Enemy = {} 298 | Alien = { width = 66.0, height = 48.0 } 299 | 300 | [[entity]] 301 | [entity.components] 302 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 303 | Transform.translation = { x = 725.0, y = 616.0 } 304 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 305 | Enemy = {} 306 | Alien = { width = 66.0, height = 48.0 } 307 | 308 | [[entity]] 309 | [entity.components] 310 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 311 | Transform.translation = { x = 815.0, y = 616.0 } 312 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 313 | Enemy = {} 314 | Alien = { width = 66.0, height = 48.0 } 315 | 316 | [[entity]] 317 | [entity.components] 318 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 3 } 319 | Transform.translation = { x = 905.0, y = 616.0 } 320 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_2", end.type = "Loop" } 321 | Enemy = {} 322 | Alien = { width = 66.0, height = 48.0 } 323 | 324 | 325 | [[entity]] 326 | [entity.components] 327 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 328 | Transform.translation = { x = 95.0, y = 688.0 } 329 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 330 | Enemy = {} 331 | Alien = { width = 48.0, height = 48.0 } 332 | 333 | [[entity]] 334 | [entity.components] 335 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 336 | Transform.translation = { x = 185.0, y = 688.0 } 337 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 338 | Enemy = {} 339 | Alien = { width = 48.0, height = 48.0 } 340 | 341 | [[entity]] 342 | [entity.components] 343 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 344 | Transform.translation = { x = 275.0, y = 688.0 } 345 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 346 | Enemy = {} 347 | Alien = { width = 48.0, height = 48.0 } 348 | 349 | [[entity]] 350 | [entity.components] 351 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 352 | Transform.translation = { x = 365.0, y = 688.0 } 353 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 354 | Enemy = {} 355 | Alien = { width = 48.0, height = 48.0 } 356 | 357 | [[entity]] 358 | [entity.components] 359 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 360 | Transform.translation = { x = 455.0, y = 688.0 } 361 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 362 | Enemy = {} 363 | Alien = { width = 48.0, height = 48.0 } 364 | 365 | [[entity]] 366 | [entity.components] 367 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 368 | Transform.translation = { x = 545.0, y = 688.0 } 369 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 370 | Enemy = {} 371 | Alien = { width = 48.0, height = 48.0 } 372 | 373 | [[entity]] 374 | [entity.components] 375 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 376 | Transform.translation = { x = 635.0, y = 688.0 } 377 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 378 | Enemy = {} 379 | Alien = { width = 48.0, height = 48.0 } 380 | 381 | [[entity]] 382 | [entity.components] 383 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 384 | Transform.translation = { x = 725.0, y = 688.0 } 385 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 386 | Enemy = {} 387 | Alien = { width = 48.0, height = 48.0 } 388 | 389 | [[entity]] 390 | [entity.components] 391 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 392 | Transform.translation = { x = 815.0, y = 688.0 } 393 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 394 | Enemy = {} 395 | Alien = { width = 48.0, height = 48.0 } 396 | 397 | [[entity]] 398 | [entity.components] 399 | SpriteRender = { sprite_sheet_name = "game", sprite_number = 6 } 400 | Transform.translation = { x = 905.0, y = 688.0 } 401 | AnimationControl = { sprite_sheet_name = "game", animation_name = "alien_loop_3", end.type = "Loop" } 402 | Enemy = {} 403 | Alien = { width = 48.0, height = 48.0 } 404 | --------------------------------------------------------------------------------