├── .github └── workflows │ └── deploy-web.yml ├── .gitignore ├── LICENSE ├── README.md ├── Taskfile.yml ├── archetype ├── airbase.go ├── bullet.go ├── camera.go ├── collectible.go ├── enemy.go ├── joystick.go ├── player.go ├── shadow.go └── wreck.go ├── assets ├── airplanes │ └── airplanes.png ├── assets.go ├── fonts │ ├── kenney-future-narrow.ttf │ └── kenney-future.ttf ├── levels │ ├── airbase.tmx │ ├── airplanes.tiled-project │ ├── airplanes.tiled-session │ ├── airplanes.tsx │ ├── level01.tmx │ ├── level02.tmx │ ├── playground.tmx │ └── tiles.tsx └── tiles │ ├── airplane_shield.png │ ├── joystick-base.png │ ├── joystick-knob.png │ └── tiles.png ├── component ├── ai.go ├── altitude.go ├── bounds.go ├── camera.go ├── collectible.go ├── collider.go ├── debug.go ├── despawnable.go ├── destroyed.go ├── distancelimit.go ├── evolution.go ├── follower.go ├── game.go ├── health.go ├── input.go ├── joystick.go ├── label.go ├── level.go ├── observer.go ├── player.go ├── playerairplane.go ├── playerselect.go ├── script.go ├── shooter.go ├── spawnable.go ├── sprite.go ├── tags.go ├── timetolive.go └── velocity.go ├── docs ├── debug.png ├── editor.png ├── evolution.gif └── screenshot.png ├── engine ├── clamp.go ├── find.go ├── hierarchy.go ├── input │ ├── input.go │ ├── touch.go │ └── touch_mobile.go ├── random.go ├── range.go ├── rect.go └── timer.go ├── game └── game.go ├── go.mod ├── go.sum ├── main.go ├── mobile ├── Airplanes │ ├── Airplanes.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcuserdata │ │ │ │ └── milosz.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Airplanes.xcscheme │ │ └── xcuserdata │ │ │ └── milosz.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── Airplanes │ │ ├── Actions.sks │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── GameScene.h │ │ ├── GameScene.m │ │ ├── GameScene.sks │ │ ├── GameViewController.h │ │ ├── GameViewController.m │ │ └── main.m │ ├── AirplanesTests │ │ └── AirplanesTests.m │ └── AirplanesUITests │ │ ├── AirplanesUITests.m │ │ └── AirplanesUITestsLaunchTests.m └── main.go ├── scene ├── airbase.go ├── game.go └── title.go ├── system ├── ai.go ├── altitude.go ├── bounds.go ├── camera.go ├── camerabounds.go ├── collision.go ├── controls.go ├── debug.go ├── despawn.go ├── destroy.go ├── events.go ├── evolution.go ├── follower.go ├── health.go ├── hud.go ├── invulnerable.go ├── label.go ├── maxdistance.go ├── observer.go ├── playerselect.go ├── progression.go ├── render.go ├── respawn.go ├── script.go ├── shooter.go ├── spawn.go ├── timetolive.go └── velocity.go └── web ├── index.html └── main.html /.github/workflows/deploy-web.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Web 2 | on: 3 | push: 4 | branches: 5 | - master 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | jobs: 11 | deploy: 12 | environment: 13 | name: github-pages 14 | url: ${{ steps.deployment.outputs.page_url }} 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-go@v2 19 | with: 20 | go-version: '^1.18' 21 | - run: go build -o web/game.wasm 22 | env: 23 | GOOS: js 24 | GOARCH: wasm 25 | - run: cp $(go env GOROOT)/misc/wasm/wasm_exec.js web/ 26 | 27 | - uses: actions/configure-pages@v2 28 | - uses: actions/upload-pages-artifact@v1 29 | with: 30 | path: web 31 | - id: deployment 32 | uses: actions/deploy-pages@v1 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.out 3 | vendor/ 4 | go.work 5 | 6 | mobile/Airplanes/Airplanes.xcodeproj/project.xcworkspace/xcuserdata 7 | mobile/Airplanes.xcframework 8 | 9 | build/ 10 | 11 | *.aar 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Miłosz Smółka 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # airplanes 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/m110/airplanes/blob/master/LICENSE) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/m110/airplanes)](https://goreportcard.com/report/github.com/m110/airplanes) 5 | [![ActionsCI](https://github.com/m110/airplanes/actions/workflows/deploy-web.yml/badge.svg)](https://github.com/m110/airplanes/actions/workflows/deploy-web.yml) 6 | [![Play online](https://img.shields.io/static/v1?label=play&message=online&color=brightgreen&logo=github)](https://m110.github.io/airplanes/) 7 | 8 | A 2D shoot 'em up game made with: 9 | 10 | * Go 11 | * [Ebitengine](https://github.com/hajimehoshi/ebiten) 12 | * [donburi](https://github.com/yohamta/donburi) 13 | * [Tiled](https://www.mapeditor.org/) and [go-tiled](https://github.com/lafriks/go-tiled) 14 | 15 | Assets by [Kenney](https://kenney.nl). 16 | 17 | ![](docs/screenshot.png) 18 | 19 | ## GitHub GameOff Disclaimer 20 | 21 | Most of this game was created during October 2022. I wanted to take part in GitHub GameOff, 22 | but sadly didn't have enough time to create a new game. 23 | 24 | I'm submitting this one and I hope this is still in the spirit of the competition since it was created pretty much within one month. 25 | You be the judge. :) 26 | 27 | ## Status 28 | 29 | Playable, with a long list of ideas and TODOs in the code. There are just two levels right now. 30 | 31 | ## How to play 32 | 33 | * Player 1: WASD to move, Space to shoot 34 | * Player 2: to move, Enter to shoot 35 | 36 | Shoot enemies and avoid bullets. 37 | 38 | Collect powerups to evolve your airplane! 39 | 40 | ![](docs/evolution.gif) 41 | 42 | ## Playing 43 | 44 | Play online: https://m110.github.io/airplanes/ 45 | 46 | Or local: 47 | 48 | ``` 49 | go run github.com/m110/airplanes@latest 50 | ``` 51 | 52 | ## Debug Mode 53 | 54 | To enable debug mode, press the slash key (`/`). 55 | 56 | ![](docs/debug.png) 57 | 58 | ## Architecture 59 | 60 | This game uses the ECS architecture provided by [donburi](https://github.com/yohamta/donburi). 61 | Parts of airplanes were ported to donburi as features. 62 | 63 | The ECS structure is as follows: 64 | 65 | * `component` - contains all components. **Components** are Go structs with no behavior other than occasional helper methods. 66 | * `system` - contains all systems. **Systems** keep the logic of the game. Each system works on entities with a specific set of components. 67 | * `archetype` - contains helper functions for creating entities with specific sets of components. 68 | 69 | Other packages: 70 | 71 | * `assets` - contains all assets. Assets are loaded using Go embedding, so they are compiled into the final binary (which makes it easier to distribute the game). 72 | * `engine` - helper functions for generic game logic. 73 | * `scenes` - contains all scenes. 74 | 75 | ## Level Editor 76 | 77 | You can edit the levels in the `assets/levels` directory using [Tiled](https://www.mapeditor.org/). 78 | 79 | Levels are loaded using the `level*.tmx` pattern, so you can add new levels by adding new files with bigger numbers. 80 | 81 | ![](docs/editor.png) 82 | 83 | Supported objects are: 84 | 85 | | Object | Description | 86 | |-------------------------|-------------------------------------------| 87 | | `enemy-airplane` | An enemy airplane. | 88 | | `enemy-tank` | An enemy tank shooting at the player. | 89 | | `enemy-turret-missiles` | An enemy turret shooting homing missiles. | 90 | | `group-spawn` | A group enemy spawn. | 91 | 92 | The rotation of objects is reflected in-game. 93 | 94 | Parameters: 95 | 96 | | Parameter | Description | 97 | |-----------|------------------------------------------------------------------------------| 98 | | `speed` | Speed of the enemy, if different than default. | 99 | | `path` | Path of the enemy. If not provided, the enemy moves in the facing direction. | 100 | | `spawn` | Group spawn, if a group of enemies needs to spawn at once. | 101 | 102 | The `path` should point to either a polygon or a polyline object in the same layer. 103 | If a polyline is used, the enemy will follow the path and keep moving in the facing direction after the last point. 104 | Polygon paths work like loops. 105 | 106 | ### Spawning 107 | 108 | Enemies are spawned once the top camera boundary reaches their position. 109 | A group spawn spawns all connected enemies at once. 110 | 111 | All objects despawn once they go out of screen. 112 | 113 | ### Mobile Development Notes 114 | 115 | 1. `task mobile` to create the `Airplanes.xcframework`. 116 | 2. Open the Xcode project and deploy. 117 | 3. If the app takes long to launch, Cmd + Shift + , and disable the "Debug executable" option. 118 | 119 | #### Creating a new mobile project 120 | 121 | 1. Create a new Xcode project (Game, SpriteKit). 122 | 2. Add the `Airplanes.xcframework` to the project as a framework. 123 | 3. Add the `GameController.framework` to the project as a framework. 124 | 4. Update `GameViewController` to use `MobileEbitenViewController`. 125 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | mobile: 5 | dir: ./mobile 6 | cmds: 7 | - ebitenmobile bind -target ios -o Airplanes.xcframework . 8 | 9 | mobile-deploy: 10 | deps: 11 | - mobile 12 | dir: ./mobile/Airplanes 13 | cmds: 14 | - xcodebuild -scheme Airplanes -configuration Debug -sdk iphoneos -derivedDataPath build 15 | - ios-deploy --bundle build/Build/Products/Debug-iphoneos/Airplanes.app --nostart --no-wifi 16 | -------------------------------------------------------------------------------- /archetype/airbase.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | "github.com/yohamta/donburi/features/transform" 7 | 8 | "github.com/m110/airplanes/assets" 9 | "github.com/m110/airplanes/component" 10 | ) 11 | 12 | func NewAirbaseAirplane(w donburi.World, position math.Vec2, faction component.PlayerFaction, index int) { 13 | airplane := w.Entry( 14 | w.Create( 15 | transform.Transform, 16 | component.Sprite, 17 | component.Velocity, 18 | component.PlayerSelect, 19 | component.Altitude, 20 | ), 21 | ) 22 | 23 | originalRotation := -90.0 24 | 25 | t := transform.Transform.Get(airplane) 26 | t.LocalPosition = position 27 | t.LocalRotation = originalRotation 28 | 29 | component.Sprite.SetValue(airplane, component.SpriteData{ 30 | Image: AirplaneImageByFaction(faction, 0), 31 | Layer: component.SpriteLayerAirUnits, 32 | Pivot: component.SpritePivotCenter, 33 | OriginalRotation: originalRotation, 34 | }) 35 | 36 | component.PlayerSelect.SetValue(airplane, component.PlayerSelectData{ 37 | Index: index, 38 | Faction: faction, 39 | }) 40 | 41 | NewCrosshair(w, airplane) 42 | 43 | shadow := NewShadow(w, airplane) 44 | transform.Transform.Get(shadow).LocalPosition = math.Vec2{} 45 | } 46 | 47 | func NewCrosshair(w donburi.World, parent *donburi.Entry) { 48 | crosshair := w.Entry( 49 | w.Create( 50 | transform.Transform, 51 | component.Sprite, 52 | ), 53 | ) 54 | 55 | transform.Transform.Get(crosshair).LocalScale = math.Vec2{X: 2.5, Y: 2.5} 56 | 57 | transform.AppendChild(parent, crosshair, false) 58 | 59 | component.Sprite.SetValue(crosshair, component.SpriteData{ 60 | Image: assets.Crosshair, 61 | Layer: component.SpriteLayerGroundUnits, 62 | Pivot: component.SpritePivotCenter, 63 | Hidden: true, 64 | }) 65 | 66 | label := w.Entry( 67 | w.Create( 68 | transform.Transform, 69 | component.Label, 70 | ), 71 | ) 72 | 73 | component.Label.SetValue(label, component.LabelData{ 74 | Text: "", 75 | Hidden: true, 76 | }) 77 | 78 | transform.AppendChild(crosshair, label, false) 79 | transform.Transform.Get(label).LocalPosition = math.Vec2{X: -25, Y: 30} 80 | } 81 | -------------------------------------------------------------------------------- /archetype/bullet.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/yohamta/donburi" 7 | "github.com/yohamta/donburi/features/math" 8 | "github.com/yohamta/donburi/features/transform" 9 | "github.com/yohamta/donburi/filter" 10 | 11 | "github.com/m110/airplanes/assets" 12 | "github.com/m110/airplanes/component" 13 | "github.com/m110/airplanes/engine" 14 | ) 15 | 16 | const ( 17 | playerBulletSpeed = 10 18 | enemyBulletSpeed = 4 19 | enemyMissileSpeed = 2 20 | ) 21 | 22 | func NewPlayerBullet(w donburi.World, player *component.PlayerData, position math.Vec2) { 23 | width := float64(assets.LaserSingle.Bounds().Dy()) 24 | 25 | if player.WeaponLevel == component.WeaponLevelSingle || 26 | player.WeaponLevel == component.WeaponLevelSingleFast { 27 | newPlayerBullet(w, math.Vec2{ 28 | X: position.X, 29 | Y: position.Y - width, 30 | }, 0) 31 | } 32 | 33 | if player.WeaponLevel == component.WeaponLevelDouble || 34 | player.WeaponLevel == component.WeaponLevelDoubleFast || 35 | player.WeaponLevel == component.WeaponLevelDiagonal || 36 | player.WeaponLevel == component.WeaponLevelDoubleDiagonal { 37 | newPlayerBullet(w, math.Vec2{ 38 | X: position.X - width/2, 39 | Y: position.Y - width/2, 40 | }, 0) 41 | newPlayerBullet(w, math.Vec2{ 42 | X: position.X + width/2, 43 | Y: position.Y - width/2, 44 | }, 0) 45 | } 46 | 47 | if player.WeaponLevel == component.WeaponLevelDiagonal || 48 | player.WeaponLevel == component.WeaponLevelDoubleDiagonal { 49 | newPlayerBullet(w, math.Vec2{ 50 | X: position.X - width, 51 | Y: position.Y - width, 52 | }, -30) 53 | newPlayerBullet(w, math.Vec2{ 54 | X: position.X + width, 55 | Y: position.Y - width, 56 | }, 30) 57 | } 58 | 59 | if player.WeaponLevel == component.WeaponLevelDoubleDiagonal { 60 | newPlayerBullet(w, math.Vec2{ 61 | X: position.X - width*1.1, 62 | Y: position.Y, 63 | }, -30) 64 | newPlayerBullet(w, math.Vec2{ 65 | X: position.X + width*1.1, 66 | Y: position.Y, 67 | }, 30) 68 | } 69 | } 70 | 71 | func newPlayerBullet(w donburi.World, position math.Vec2, localRotation float64) { 72 | bullet := w.Entry( 73 | w.Create( 74 | component.Velocity, 75 | transform.Transform, 76 | component.Sprite, 77 | component.Despawnable, 78 | component.Collider, 79 | component.DistanceLimit, 80 | ), 81 | ) 82 | 83 | image := assets.LaserSingle 84 | 85 | originalRotation := -90.0 86 | 87 | t := transform.Transform.Get(bullet) 88 | t.LocalPosition = position 89 | t.LocalRotation = originalRotation + localRotation 90 | 91 | component.Velocity.SetValue(bullet, component.VelocityData{ 92 | Velocity: transform.Right(bullet).MulScalar(playerBulletSpeed), 93 | }) 94 | 95 | component.Sprite.SetValue(bullet, component.SpriteData{ 96 | Image: image, 97 | Layer: component.SpriteLayerAirUnits, 98 | Pivot: component.SpritePivotCenter, 99 | OriginalRotation: originalRotation, 100 | }) 101 | 102 | width, height := image.Size() 103 | 104 | component.Collider.SetValue(bullet, component.ColliderData{ 105 | Width: float64(width), 106 | Height: float64(height), 107 | Layer: component.CollisionLayerPlayerBullets, 108 | }) 109 | 110 | component.DistanceLimit.SetValue(bullet, component.DistanceLimitData{ 111 | MaxDistance: 200, 112 | }) 113 | } 114 | 115 | func NewEnemyBullet(w donburi.World, position math.Vec2, rotation float64) { 116 | bullet := w.Entry( 117 | w.Create( 118 | component.Velocity, 119 | transform.Transform, 120 | component.Sprite, 121 | component.Despawnable, 122 | component.Collider, 123 | ), 124 | ) 125 | 126 | image := assets.Bullet 127 | 128 | t := transform.Transform.Get(bullet) 129 | t.LocalPosition = position 130 | t.LocalRotation = rotation 131 | 132 | component.Velocity.SetValue(bullet, component.VelocityData{ 133 | Velocity: transform.Right(bullet).MulScalar(enemyBulletSpeed), 134 | }) 135 | 136 | component.Sprite.SetValue(bullet, component.SpriteData{ 137 | Image: image, 138 | Layer: component.SpriteLayerAirUnits, 139 | Pivot: component.SpritePivotCenter, 140 | OriginalRotation: -90, 141 | }) 142 | 143 | width, height := image.Size() 144 | 145 | component.Collider.SetValue(bullet, component.ColliderData{ 146 | Width: float64(width), 147 | Height: float64(height), 148 | Layer: component.CollisionLayerEnemyBullets, 149 | }) 150 | } 151 | 152 | func NewEnemyMissile(w donburi.World, position math.Vec2, rotation float64) { 153 | missile := w.Entry( 154 | w.Create( 155 | component.Velocity, 156 | transform.Transform, 157 | component.Sprite, 158 | component.Despawnable, 159 | component.Collider, 160 | component.Follower, 161 | ), 162 | ) 163 | 164 | image := assets.Missile 165 | 166 | t := transform.Transform.Get(missile) 167 | t.LocalPosition = position 168 | t.LocalRotation = rotation 169 | 170 | component.Velocity.SetValue(missile, component.VelocityData{ 171 | Velocity: transform.Right(missile).MulScalar(enemyMissileSpeed), 172 | }) 173 | 174 | component.Sprite.SetValue(missile, component.SpriteData{ 175 | Image: image, 176 | Layer: component.SpriteLayerAirUnits, 177 | Pivot: component.SpritePivotCenter, 178 | OriginalRotation: -90, 179 | }) 180 | 181 | width, height := image.Size() 182 | 183 | component.Collider.SetValue(missile, component.ColliderData{ 184 | Width: float64(width), 185 | Height: float64(height), 186 | Layer: component.CollisionLayerEnemyBullets, 187 | }) 188 | 189 | component.Follower.SetValue(missile, component.FollowerData{ 190 | Target: component.ClosestTarget(w, missile, donburi.NewQuery(filter.Contains(component.PlayerAirplane))), 191 | FollowingSpeed: enemyMissileSpeed, 192 | FollowingTimer: engine.NewTimer(3 * time.Second), 193 | }) 194 | } 195 | -------------------------------------------------------------------------------- /archetype/camera.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/yohamta/donburi" 7 | "github.com/yohamta/donburi/features/math" 8 | "github.com/yohamta/donburi/features/transform" 9 | "github.com/yohamta/donburi/filter" 10 | 11 | "github.com/m110/airplanes/component" 12 | "github.com/m110/airplanes/engine" 13 | ) 14 | 15 | func NewCamera(w donburi.World, startPosition math.Vec2) *donburi.Entry { 16 | camera := w.Entry( 17 | w.Create( 18 | transform.Transform, 19 | component.Velocity, 20 | component.Camera, 21 | ), 22 | ) 23 | 24 | cameraCamera := component.Camera.Get(camera) 25 | cameraCamera.MoveTimer = engine.NewTimer(time.Second * 3) 26 | transform.Transform.Get(camera).LocalPosition = startPosition 27 | 28 | return camera 29 | } 30 | 31 | func MustFindCamera(w donburi.World) *donburi.Entry { 32 | camera, ok := donburi.NewQuery(filter.Contains(component.Camera)).First(w) 33 | if !ok { 34 | panic("no camera found") 35 | } 36 | 37 | return camera 38 | } 39 | -------------------------------------------------------------------------------- /archetype/collectible.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/yohamta/donburi" 8 | "github.com/yohamta/donburi/features/math" 9 | "github.com/yohamta/donburi/features/transform" 10 | 11 | "github.com/m110/airplanes/assets" 12 | "github.com/m110/airplanes/component" 13 | ) 14 | 15 | func NewRandomCollectible(w donburi.World, position math.Vec2) { 16 | collectible := w.Entry(w.Create( 17 | transform.Transform, 18 | component.Sprite, 19 | component.Collider, 20 | component.Collectible, 21 | component.Despawnable, 22 | )) 23 | 24 | var image *ebiten.Image 25 | var collectibleType component.CollectibleType 26 | switch rand.Intn(3) { 27 | case 0: 28 | image = assets.WeaponUpgrade 29 | collectibleType = component.CollectibleTypeWeaponUpgrade 30 | case 1: 31 | image = assets.Shield 32 | collectibleType = component.CollectibleTypeShield 33 | case 2: 34 | image = assets.Health 35 | collectibleType = component.CollectibleTypeHealth 36 | } 37 | 38 | transform.Transform.Get(collectible).LocalPosition = position 39 | 40 | component.Sprite.SetValue(collectible, component.SpriteData{ 41 | Image: image, 42 | Layer: component.SpriteLayerCollectibles, 43 | Pivot: component.SpritePivotCenter, 44 | }) 45 | 46 | width, height := image.Size() 47 | component.Collider.SetValue(collectible, component.ColliderData{ 48 | Width: float64(width), 49 | Height: float64(height), 50 | Layer: component.CollisionLayerCollectibles, 51 | }) 52 | 53 | component.Collectible.SetValue(collectible, component.CollectibleData{ 54 | Type: collectibleType, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /archetype/enemy.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/yohamta/donburi" 8 | "github.com/yohamta/donburi/features/math" 9 | "github.com/yohamta/donburi/features/transform" 10 | "github.com/yohamta/donburi/filter" 11 | 12 | "github.com/m110/airplanes/assets" 13 | "github.com/m110/airplanes/component" 14 | "github.com/m110/airplanes/engine" 15 | ) 16 | 17 | func NewEnemySpawn(w donburi.World, position math.Vec2, spawnFunc component.SpawnFunc) { 18 | spawn := w.Entry( 19 | w.Create( 20 | transform.Transform, 21 | component.Spawnable, 22 | ), 23 | ) 24 | 25 | transform.Transform.Get(spawn).LocalPosition = position 26 | component.Spawnable.Get(spawn).SpawnFunc = spawnFunc 27 | } 28 | 29 | func NewEnemyAirplane( 30 | w donburi.World, 31 | position math.Vec2, 32 | rotation float64, 33 | speed float64, 34 | path assets.Path, 35 | ) { 36 | airplane := w.Entry( 37 | w.Create( 38 | transform.Transform, 39 | component.Velocity, 40 | component.Sprite, 41 | component.AI, 42 | component.Despawnable, 43 | component.Collider, 44 | component.Health, 45 | component.Wreckable, 46 | ), 47 | ) 48 | 49 | originalRotation := -90.0 50 | 51 | t := transform.Transform.Get(airplane) 52 | t.LocalPosition = position 53 | t.LocalRotation = originalRotation + rotation 54 | 55 | image := assets.AirplaneGraySmall 56 | component.Sprite.SetValue(airplane, component.SpriteData{ 57 | Image: image, 58 | Layer: component.SpriteLayerAirUnits, 59 | Pivot: component.SpritePivotCenter, 60 | OriginalRotation: originalRotation, 61 | }) 62 | 63 | width, height := image.Size() 64 | 65 | component.Collider.SetValue(airplane, component.ColliderData{ 66 | Width: float64(width), 67 | Height: float64(height), 68 | Layer: component.CollisionLayerAirEnemies, 69 | }) 70 | 71 | if len(path.Points) > 0 { 72 | component.AI.SetValue(airplane, component.AIData{ 73 | Type: component.AITypeFollowPath, 74 | Speed: speed, 75 | Path: path.Points, 76 | PathLoops: path.Loops, 77 | }) 78 | } else { 79 | component.AI.SetValue(airplane, component.AIData{ 80 | Type: component.AITypeConstantVelocity, 81 | Speed: speed, 82 | }) 83 | } 84 | 85 | health := component.Health.Get(airplane) 86 | health.Health = 3 87 | health.DamageIndicator = newDamageIndicator(w, airplane) 88 | 89 | NewShadow(w, airplane) 90 | } 91 | 92 | func NewEnemyTank( 93 | w donburi.World, 94 | position math.Vec2, 95 | rotation float64, 96 | speed float64, 97 | path assets.Path, 98 | ) { 99 | tank := w.Entry( 100 | w.Create( 101 | transform.Transform, 102 | component.Velocity, 103 | component.Sprite, 104 | component.AI, 105 | component.Despawnable, 106 | component.Collider, 107 | component.Health, 108 | ), 109 | ) 110 | 111 | t := transform.Transform.Get(tank) 112 | t.LocalPosition = position 113 | t.LocalRotation = rotation 114 | 115 | image := assets.TankBase 116 | component.Sprite.SetValue(tank, component.SpriteData{ 117 | Image: image, 118 | Layer: component.SpriteLayerGroundUnits, 119 | Pivot: component.SpritePivotCenter, 120 | OriginalRotation: 0, 121 | }) 122 | 123 | width, height := image.Size() 124 | 125 | component.Collider.SetValue(tank, component.ColliderData{ 126 | Width: float64(width), 127 | Height: float64(height), 128 | Layer: component.CollisionLayerGroundEnemies, 129 | }) 130 | 131 | if len(path.Points) > 0 { 132 | component.AI.SetValue(tank, component.AIData{ 133 | Type: component.AITypeFollowPath, 134 | Speed: speed, 135 | Path: path.Points, 136 | PathLoops: path.Loops, 137 | }) 138 | } else { 139 | component.AI.SetValue(tank, component.AIData{ 140 | Type: component.AITypeConstantVelocity, 141 | Speed: speed, 142 | }) 143 | } 144 | 145 | health := component.Health.Get(tank) 146 | health.Health = 5 147 | health.DamageIndicator = newDamageIndicator(w, tank) 148 | 149 | gun := w.Entry( 150 | w.Create( 151 | transform.Transform, 152 | component.Sprite, 153 | component.Despawnable, 154 | component.Observer, 155 | component.Shooter, 156 | ), 157 | ) 158 | 159 | originalRotation := 90.0 160 | gunT := transform.Transform.Get(gun) 161 | gunT.LocalPosition = position 162 | gunT.LocalRotation = originalRotation + rotation 163 | 164 | component.Sprite.SetValue(gun, component.SpriteData{ 165 | Image: assets.TankGun, 166 | Layer: component.SpriteLayerGroundGuns, 167 | Pivot: component.SpritePivotCenter, 168 | OriginalRotation: originalRotation, 169 | }) 170 | 171 | component.Observer.SetValue(gun, component.ObserverData{ 172 | LookFor: donburi.NewQuery(filter.Contains(component.PlayerAirplane)), 173 | }) 174 | 175 | component.Shooter.SetValue(gun, component.ShooterData{ 176 | Type: component.ShooterTypeBullet, 177 | ShootTimer: engine.NewTimer(time.Millisecond * 2500), 178 | }) 179 | 180 | transform.AppendChild(tank, gun, true) 181 | } 182 | 183 | func NewEnemyTurretBeam( 184 | w donburi.World, 185 | position math.Vec2, 186 | rotation float64, 187 | ) { 188 | turret := newEnemyTurret(w, position, rotation) 189 | 190 | gun := w.Entry( 191 | w.Create( 192 | transform.Transform, 193 | component.Sprite, 194 | component.Despawnable, 195 | component.Observer, 196 | component.Shooter, 197 | ), 198 | ) 199 | 200 | originalRotation := 90.0 201 | gunT := transform.Transform.Get(gun) 202 | gunT.LocalPosition = position 203 | gunT.LocalRotation = originalRotation + rotation 204 | 205 | component.Sprite.SetValue(gun, component.SpriteData{ 206 | Image: assets.TurretGunSingle, 207 | Layer: component.SpriteLayerGroundGuns, 208 | Pivot: component.SpritePivotCenter, 209 | OriginalRotation: originalRotation, 210 | }) 211 | 212 | component.Observer.SetValue(gun, component.ObserverData{ 213 | LookFor: donburi.NewQuery(filter.Contains(component.PlayerAirplane)), 214 | }) 215 | 216 | component.Shooter.SetValue(gun, component.ShooterData{ 217 | Type: component.ShooterTypeBeam, 218 | ShootTimer: engine.NewTimer(time.Millisecond * 5000), 219 | }) 220 | 221 | transform.AppendChild(turret, gun, true) 222 | } 223 | 224 | func NewEnemyTurretMissiles( 225 | w donburi.World, 226 | position math.Vec2, 227 | rotation float64, 228 | ) { 229 | turret := newEnemyTurret(w, position, rotation) 230 | 231 | gun := w.Entry( 232 | w.Create( 233 | transform.Transform, 234 | component.Sprite, 235 | component.Despawnable, 236 | component.Observer, 237 | component.Shooter, 238 | ), 239 | ) 240 | 241 | originalRotation := 90.0 242 | gunT := transform.Transform.Get(gun) 243 | gunT.LocalPosition = position 244 | gunT.LocalRotation = originalRotation + rotation 245 | 246 | component.Sprite.SetValue(gun, component.SpriteData{ 247 | Image: assets.TurretGunDouble, 248 | Layer: component.SpriteLayerGroundGuns, 249 | Pivot: component.SpritePivotCenter, 250 | OriginalRotation: originalRotation, 251 | }) 252 | 253 | component.Observer.SetValue(gun, component.ObserverData{ 254 | LookFor: donburi.NewQuery(filter.Contains(component.PlayerAirplane)), 255 | }) 256 | 257 | component.Shooter.SetValue(gun, component.ShooterData{ 258 | Type: component.ShooterTypeMissile, 259 | ShootTimer: engine.NewTimer(time.Millisecond * 5000), 260 | }) 261 | 262 | transform.AppendChild(turret, gun, true) 263 | } 264 | 265 | func newEnemyTurret( 266 | w donburi.World, 267 | position math.Vec2, 268 | rotation float64, 269 | ) *donburi.Entry { 270 | turret := w.Entry( 271 | w.Create( 272 | transform.Transform, 273 | component.Sprite, 274 | component.AI, 275 | component.Despawnable, 276 | component.Collider, 277 | component.Health, 278 | ), 279 | ) 280 | 281 | t := transform.Transform.Get(turret) 282 | t.LocalPosition = position 283 | t.LocalRotation = rotation 284 | 285 | image := assets.TurretBase 286 | component.Sprite.SetValue(turret, component.SpriteData{ 287 | Image: image, 288 | Layer: component.SpriteLayerGroundUnits, 289 | Pivot: component.SpritePivotCenter, 290 | OriginalRotation: 0, 291 | }) 292 | 293 | width, height := image.Size() 294 | 295 | component.Collider.SetValue(turret, component.ColliderData{ 296 | Width: float64(width), 297 | Height: float64(height), 298 | Layer: component.CollisionLayerGroundEnemies, 299 | }) 300 | 301 | health := component.Health.Get(turret) 302 | health.Health = 5 303 | health.DamageIndicator = newDamageIndicator(w, turret) 304 | 305 | return turret 306 | } 307 | 308 | func newDamageIndicator(w donburi.World, parent *donburi.Entry) *component.SpriteData { 309 | indicator := w.Entry( 310 | w.Create( 311 | transform.Transform, 312 | component.Sprite, 313 | ), 314 | ) 315 | 316 | parentSprite := component.Sprite.Get(parent) 317 | 318 | image := ebiten.NewImage(parentSprite.Image.Size()) 319 | op := &ebiten.DrawImageOptions{} 320 | op.ColorM.Translate(1, 1, 1, 0) 321 | image.DrawImage(parentSprite.Image, op) 322 | 323 | component.Sprite.SetValue(indicator, component.SpriteData{ 324 | Image: image, 325 | Layer: component.SpriteLayerIndicators, 326 | Pivot: parentSprite.Pivot, 327 | OriginalRotation: parentSprite.OriginalRotation, 328 | Hidden: true, 329 | }) 330 | 331 | transform.AppendChild(parent, indicator, false) 332 | 333 | return component.Sprite.Get(indicator) 334 | } 335 | -------------------------------------------------------------------------------- /archetype/joystick.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | "github.com/yohamta/donburi/features/transform" 7 | 8 | "github.com/m110/airplanes/assets" 9 | "github.com/m110/airplanes/component" 10 | ) 11 | 12 | func NewJoystick(w donburi.World, pos math.Vec2) *donburi.Entry { 13 | joystick := w.Entry(w.Create( 14 | transform.Transform, 15 | component.Joystick, 16 | component.Sprite, 17 | component.UI, 18 | )) 19 | component.Sprite.SetValue(joystick, component.SpriteData{ 20 | Image: assets.JoystickBase, 21 | Layer: component.SpriteLayerUI, 22 | Pivot: component.SpritePivotCenter, 23 | }) 24 | t := transform.Transform.Get(joystick) 25 | t.LocalPosition = pos 26 | t.LocalScale = math.Vec2{X: 0.2, Y: 0.2} 27 | 28 | knob := w.Entry(w.Create( 29 | transform.Transform, 30 | component.Joystick, 31 | component.Sprite, 32 | component.UI, 33 | )) 34 | 35 | component.Sprite.SetValue(knob, component.SpriteData{ 36 | Image: assets.JoystickKnob, 37 | Layer: component.SpriteLayerUI, 38 | Pivot: component.SpritePivotCenter, 39 | }) 40 | transform.AppendChild(joystick, knob, false) 41 | 42 | return joystick 43 | } 44 | -------------------------------------------------------------------------------- /archetype/player.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/yohamta/donburi" 9 | "github.com/yohamta/donburi/features/math" 10 | "github.com/yohamta/donburi/features/transform" 11 | "github.com/yohamta/donburi/filter" 12 | 13 | "github.com/m110/airplanes/assets" 14 | "github.com/m110/airplanes/component" 15 | "github.com/m110/airplanes/engine" 16 | ) 17 | 18 | type PlayerInputs struct { 19 | Up ebiten.Key 20 | Right ebiten.Key 21 | Down ebiten.Key 22 | Left ebiten.Key 23 | Shoot ebiten.Key 24 | } 25 | 26 | type PlayerSettings struct { 27 | Inputs PlayerInputs 28 | } 29 | 30 | func AirplaneImageByFaction(faction component.PlayerFaction, level int) *ebiten.Image { 31 | switch faction { 32 | case component.PlayerFactionBlue: 33 | return assets.AirplanesBlue[level] 34 | case component.PlayerFactionRed: 35 | return assets.AirplanesRed[level] 36 | case component.PlayerFactionGreen: 37 | return assets.AirplanesGreen[level] 38 | case component.PlayerFactionYellow: 39 | return assets.AirplanesYellow[level] 40 | default: 41 | panic(fmt.Sprintf("unknown player faction: %v", faction)) 42 | } 43 | } 44 | 45 | var Players = map[int]PlayerSettings{ 46 | 1: { 47 | Inputs: PlayerInputs{ 48 | Up: ebiten.KeyW, 49 | Right: ebiten.KeyD, 50 | Down: ebiten.KeyS, 51 | Left: ebiten.KeyA, 52 | Shoot: ebiten.KeySpace, 53 | }, 54 | }, 55 | 2: { 56 | Inputs: PlayerInputs{ 57 | Up: ebiten.KeyUp, 58 | Right: ebiten.KeyRight, 59 | Down: ebiten.KeyDown, 60 | Left: ebiten.KeyLeft, 61 | Shoot: ebiten.KeyEnter, 62 | }, 63 | }, 64 | } 65 | 66 | func playerSpawn(w donburi.World, playerNumber int) math.Vec2 { 67 | game := component.MustFindGame(w) 68 | cameraPos := transform.Transform.Get(MustFindCamera(w)).LocalPosition 69 | 70 | switch playerNumber { 71 | case 1: 72 | return math.Vec2{ 73 | X: float64(game.Settings.ScreenWidth) * 0.25, 74 | Y: cameraPos.Y + float64(game.Settings.ScreenHeight)*0.9, 75 | } 76 | case 2: 77 | return math.Vec2{ 78 | X: float64(game.Settings.ScreenWidth) * 0.75, 79 | Y: cameraPos.Y + float64(game.Settings.ScreenHeight)*0.9, 80 | } 81 | default: 82 | panic(fmt.Sprintf("unknown player number: %v", playerNumber)) 83 | } 84 | } 85 | 86 | func NewPlayer(w donburi.World, playerNumber int, faction component.PlayerFaction) *donburi.Entry { 87 | _, ok := Players[playerNumber] 88 | if !ok { 89 | panic(fmt.Sprintf("unknown player number: %v", playerNumber)) 90 | } 91 | 92 | player := component.PlayerData{ 93 | PlayerNumber: playerNumber, 94 | PlayerFaction: faction, 95 | Lives: 3, 96 | RespawnTimer: engine.NewTimer(time.Second * 3), 97 | WeaponLevel: component.WeaponLevelSingle, 98 | } 99 | 100 | // TODO It looks like a constructor would fit here 101 | player.ShootTimer = engine.NewTimer(player.WeaponCooldown()) 102 | 103 | return NewPlayerFromPlayerData(w, player) 104 | } 105 | 106 | func NewPlayerFromPlayerData(w donburi.World, playerData component.PlayerData) *donburi.Entry { 107 | player := w.Entry(w.Create(component.Player)) 108 | component.Player.SetValue(player, playerData) 109 | return player 110 | } 111 | 112 | func NewPlayerAirplane(w donburi.World, player component.PlayerData, faction component.PlayerFaction, evolutionLevel int) { 113 | settings, ok := Players[player.PlayerNumber] 114 | if !ok { 115 | panic(fmt.Sprintf("unknown player number: %v", player.PlayerNumber)) 116 | } 117 | 118 | airplane := w.Entry( 119 | w.Create( 120 | component.PlayerAirplane, 121 | transform.Transform, 122 | component.Velocity, 123 | component.Sprite, 124 | component.Input, 125 | component.Bounds, 126 | component.Collider, 127 | component.Evolution, 128 | component.Wreckable, 129 | ), 130 | ) 131 | 132 | shield := w.Entry( 133 | w.Create( 134 | transform.Transform, 135 | component.Sprite, 136 | ), 137 | ) 138 | 139 | component.Sprite.SetValue(shield, component.SpriteData{ 140 | Image: assets.AirplaneShield, 141 | Layer: component.SpriteLayerIndicators, 142 | Pivot: component.SpritePivotCenter, 143 | OriginalRotation: -90.0, 144 | }) 145 | 146 | component.PlayerAirplane.SetValue(airplane, component.PlayerAirplaneData{ 147 | PlayerNumber: player.PlayerNumber, 148 | Faction: faction, 149 | InvulnerableTimer: engine.NewTimer(time.Second * 3), 150 | InvulnerableIndicator: component.Sprite.Get(shield), 151 | }) 152 | 153 | component.PlayerAirplane.Get(airplane).StartInvulnerability() 154 | 155 | originalRotation := -90.0 156 | 157 | pos := playerSpawn(w, player.PlayerNumber) 158 | t := transform.Transform.Get(airplane) 159 | t.LocalPosition = pos 160 | t.LocalRotation = originalRotation 161 | 162 | transform.AppendChild(airplane, shield, false) 163 | 164 | image := AirplaneImageByFaction(faction, evolutionLevel) 165 | 166 | component.Sprite.SetValue(airplane, component.SpriteData{ 167 | Image: image, 168 | Layer: component.SpriteLayerAirUnits, 169 | Pivot: component.SpritePivotCenter, 170 | OriginalRotation: originalRotation, 171 | }) 172 | 173 | width, height := image.Size() 174 | component.Collider.SetValue(airplane, component.ColliderData{ 175 | Width: float64(width), 176 | Height: float64(height), 177 | Layer: component.CollisionLayerPlayers, 178 | }) 179 | 180 | inputs := settings.Inputs 181 | component.Input.SetValue(airplane, component.InputData{ 182 | MoveUpKey: inputs.Up, 183 | MoveRightKey: inputs.Right, 184 | MoveDownKey: inputs.Down, 185 | MoveLeftKey: inputs.Left, 186 | MoveSpeed: 3.5, 187 | ShootKey: inputs.Shoot, 188 | }) 189 | 190 | component.Evolution.SetValue(airplane, component.EvolutionData{ 191 | Level: evolutionLevel, 192 | GrowTimer: engine.NewTimer(time.Second * 1), 193 | ShrinkTimer: engine.NewTimer(time.Second * 1), 194 | }) 195 | 196 | NewShadow(w, airplane) 197 | 198 | evolutions := []*donburi.Entry{ 199 | w.Entry( 200 | w.Create( 201 | transform.Transform, 202 | component.Sprite, 203 | component.CurrentEvolutionTag, 204 | ), 205 | ), 206 | w.Entry( 207 | w.Create( 208 | transform.Transform, 209 | component.Sprite, 210 | component.NextEvolutionTag, 211 | ), 212 | ), 213 | } 214 | 215 | for i := range evolutions { 216 | e := evolutions[i] 217 | 218 | transform.AppendChild(airplane, e, false) 219 | 220 | component.Sprite.SetValue(e, component.SpriteData{ 221 | Image: ebiten.NewImageFromImage(image), 222 | Layer: component.SpriteLayerAirUnits, 223 | Pivot: component.SpritePivotCenter, 224 | OriginalRotation: originalRotation, 225 | Hidden: true, 226 | }) 227 | } 228 | } 229 | 230 | func MustFindPlayerByNumber(w donburi.World, playerNumber int) *component.PlayerData { 231 | var foundPlayer *component.PlayerData 232 | donburi.NewQuery(filter.Contains(component.Player)).Each(w, func(e *donburi.Entry) { 233 | player := component.Player.Get(e) 234 | if player.PlayerNumber == playerNumber { 235 | foundPlayer = player 236 | } 237 | }) 238 | 239 | if foundPlayer == nil { 240 | panic(fmt.Sprintf("player not found: %v", playerNumber)) 241 | } 242 | 243 | return foundPlayer 244 | } 245 | -------------------------------------------------------------------------------- /archetype/shadow.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/yohamta/donburi" 6 | "github.com/yohamta/donburi/features/math" 7 | "github.com/yohamta/donburi/features/transform" 8 | 9 | "github.com/m110/airplanes/component" 10 | ) 11 | 12 | const ( 13 | shadowColorScale = 0.5 14 | shadowColorAlpha = 0.4 15 | 16 | // TODO Should this be based on the sprite's width? 17 | MaxShadowPosition = 12 18 | ) 19 | 20 | func NewShadow(w donburi.World, parent *donburi.Entry) *donburi.Entry { 21 | shadow := w.Entry( 22 | w.Create( 23 | transform.Transform, 24 | component.Sprite, 25 | component.ShadowTag, 26 | ), 27 | ) 28 | 29 | transform.AppendChild(parent, shadow, false) 30 | 31 | transform := transform.Transform.Get(shadow) 32 | transform.LocalPosition = math.Vec2{ 33 | X: -MaxShadowPosition, 34 | Y: MaxShadowPosition, 35 | } 36 | 37 | parentSprite := component.Sprite.Get(parent) 38 | 39 | spriteData := component.SpriteData{ 40 | Image: ShadowImage(parentSprite.Image), 41 | Layer: component.SpriteLayerShadows, 42 | Pivot: parentSprite.Pivot, 43 | OriginalRotation: parentSprite.OriginalRotation, 44 | } 45 | 46 | component.Sprite.SetValue(shadow, spriteData) 47 | 48 | return shadow 49 | } 50 | 51 | func ShadowImage(source *ebiten.Image) *ebiten.Image { 52 | shadow := ebiten.NewImage(source.Size()) 53 | op := &ebiten.DrawImageOptions{} 54 | ShadowDrawOptions(op) 55 | shadow.DrawImage(source, op) 56 | return shadow 57 | } 58 | 59 | func ShadowDrawOptions(op *ebiten.DrawImageOptions) { 60 | op.ColorM.Scale(0, 0, 0, shadowColorAlpha) 61 | op.ColorM.Translate(shadowColorScale, shadowColorScale, shadowColorScale, 0) 62 | } 63 | -------------------------------------------------------------------------------- /archetype/wreck.go: -------------------------------------------------------------------------------- 1 | package archetype 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/yohamta/donburi" 8 | "github.com/yohamta/donburi/features/transform" 9 | 10 | "github.com/m110/airplanes/component" 11 | "github.com/m110/airplanes/engine" 12 | ) 13 | 14 | func NewAirplaneWreck(w donburi.World, parent *donburi.Entry, sprite *component.SpriteData) { 15 | widthInt, heightInt := sprite.Image.Size() 16 | width, height := float64(widthInt), float64(heightInt) 17 | cutpointX := float64(int(width * engine.RandomFloatRange(0.3, 0.7))) 18 | cutpointY := float64(int(height * engine.RandomFloatRange(0.3, 0.7))) 19 | 20 | pieces := []engine.Rect{ 21 | { 22 | X: 0, 23 | Y: 0, 24 | Width: cutpointX, 25 | Height: cutpointY, 26 | }, 27 | { 28 | X: cutpointX, 29 | Y: 0, 30 | Width: width - cutpointX, 31 | Height: cutpointY, 32 | }, 33 | { 34 | X: 0, 35 | Y: cutpointY, 36 | Width: cutpointX, 37 | Height: height - cutpointY, 38 | }, 39 | { 40 | X: cutpointX, 41 | Y: cutpointY, 42 | Width: width - cutpointX, 43 | Height: height - cutpointY, 44 | }, 45 | } 46 | 47 | halfW := width / 2 48 | halfH := height / 2 49 | 50 | // Rotate the base image 51 | baseImage := ebiten.NewImage(sprite.Image.Size()) 52 | op := &ebiten.DrawImageOptions{} 53 | op.GeoM.Translate(-halfW, -halfH) 54 | op.GeoM.Rotate(float64(int(transform.WorldRotation(parent)-sprite.OriginalRotation)%360) * 2 * math.Pi / 360) 55 | op.GeoM.Translate(halfW, halfH) 56 | baseImage.DrawImage(sprite.Image, op) 57 | 58 | basePos := transform.WorldPosition(parent) 59 | if sprite.Pivot == component.SpritePivotCenter { 60 | basePos.X -= halfW 61 | basePos.Y -= halfH 62 | } 63 | 64 | for _, p := range pieces { 65 | img := baseImage.SubImage(p.ToImageRectangle()).(*ebiten.Image) 66 | 67 | wreck := w.Entry( 68 | w.Create( 69 | transform.Transform, 70 | component.Velocity, 71 | component.Altitude, 72 | component.Sprite, 73 | ), 74 | ) 75 | 76 | pos := basePos 77 | pos.X += p.X + p.Width/2 78 | pos.Y += p.Y + p.Height/2 79 | 80 | transform.Transform.Get(wreck).LocalPosition = pos 81 | 82 | component.Sprite.SetValue(wreck, component.SpriteData{ 83 | Image: img, 84 | Layer: component.SpriteLayerFallingWrecks, 85 | Pivot: sprite.Pivot, 86 | }) 87 | 88 | velocity := transform.Right(parent) 89 | velocity.X *= engine.RandomFloatRange(0.5, 0.8) 90 | velocity.Y *= engine.RandomFloatRange(0.5, 0.8) 91 | 92 | component.Velocity.SetValue(wreck, component.VelocityData{ 93 | Velocity: velocity, 94 | RotationVelocity: engine.RandomFloatRange(-2, 2), 95 | }) 96 | 97 | component.Altitude.SetValue(wreck, component.AltitudeData{ 98 | Altitude: 1.0, 99 | Velocity: -0.01, 100 | Falling: true, 101 | }) 102 | 103 | NewShadow(w, wreck) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /assets/airplanes/airplanes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/airplanes/airplanes.png -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "fmt" 7 | "image" 8 | _ "image/png" 9 | "io/fs" 10 | "path/filepath" 11 | "strconv" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | "github.com/lafriks/go-tiled" 15 | "github.com/lafriks/go-tiled/render" 16 | "github.com/yohamta/donburi/features/math" 17 | "golang.org/x/image/font" 18 | "golang.org/x/image/font/opentype" 19 | ) 20 | 21 | var ( 22 | //go:embed tiles/airplane_shield.png 23 | airplaneShieldData []byte 24 | 25 | //go:embed tiles/joystick-base.png 26 | joystickBaseData []byte 27 | //go:embed tiles/joystick-knob.png 28 | joystickKnobData []byte 29 | 30 | //go:embed fonts/kenney-future.ttf 31 | normalFontData []byte 32 | //go:embed fonts/kenney-future-narrow.ttf 33 | narrowFontData []byte 34 | 35 | //go:embed * 36 | assetsFS embed.FS 37 | 38 | AirplanesBlue []*ebiten.Image 39 | AirplanesRed []*ebiten.Image 40 | AirplanesGreen []*ebiten.Image 41 | AirplanesYellow []*ebiten.Image 42 | 43 | AirplaneGraySmall *ebiten.Image 44 | 45 | TankBase *ebiten.Image 46 | TankGun *ebiten.Image 47 | 48 | TurretBase *ebiten.Image 49 | TurretGunSingle *ebiten.Image 50 | TurretGunDouble *ebiten.Image 51 | 52 | LaserSingle *ebiten.Image 53 | Bullet *ebiten.Image 54 | Missile *ebiten.Image 55 | 56 | Health *ebiten.Image 57 | WeaponUpgrade *ebiten.Image 58 | Shield *ebiten.Image 59 | 60 | AirplaneShield *ebiten.Image 61 | Crosshair *ebiten.Image 62 | 63 | JoystickBase *ebiten.Image 64 | JoystickKnob *ebiten.Image 65 | 66 | AirBase AirBaseLevel 67 | Levels []Level 68 | 69 | SmallFont font.Face 70 | NormalFont font.Face 71 | NarrowFont font.Face 72 | ) 73 | 74 | const ( 75 | EnemyClassAirplane = "enemy-airplane" 76 | EnemyClassTank = "enemy-tank" 77 | EnemyClassTurretBeam = "enemy-turret-beam" 78 | EnemyClassTurretMissiles = "enemy-turret-missiles" 79 | 80 | ObjectClassGroupSpawn = "group-spawn" 81 | 82 | TilesetClassTiles = "tiles" 83 | TilesetClassAirplanes = "airplanes" 84 | ) 85 | 86 | type Spawn struct { 87 | Faction string 88 | Position math.Vec2 89 | } 90 | 91 | type AirBaseLevel struct { 92 | Background *ebiten.Image 93 | Spawns []Spawn 94 | } 95 | 96 | type Level struct { 97 | Background *ebiten.Image 98 | Enemies []Enemy 99 | EnemyGroupSpawns []EnemyGroupSpawn 100 | } 101 | 102 | type EnemyGroupSpawn struct { 103 | Position math.Vec2 104 | Enemies []Enemy 105 | } 106 | 107 | type Enemy struct { 108 | Class string 109 | Position math.Vec2 110 | Rotation float64 111 | Speed float64 112 | Path Path 113 | } 114 | 115 | type Path struct { 116 | Points []math.Vec2 117 | Loops bool 118 | } 119 | 120 | func MustLoadAssets() { 121 | loader := newLevelLoader() 122 | AirBase = loader.MustLoadAirBaseLevel() 123 | Levels = loader.MustLoadLevels() 124 | 125 | SmallFont = mustLoadFont(normalFontData, 10) 126 | NormalFont = mustLoadFont(normalFontData, 24) 127 | NarrowFont = mustLoadFont(narrowFontData, 24) 128 | 129 | AirplanesBlue = []*ebiten.Image{ 130 | loader.MustFindTile(TilesetClassAirplanes, "airplane-blue-small"), 131 | loader.MustFindTile(TilesetClassAirplanes, "airplane-blue-medium"), 132 | loader.MustFindTile(TilesetClassAirplanes, "airplane-blue-big"), 133 | } 134 | AirplanesRed = []*ebiten.Image{ 135 | loader.MustFindTile(TilesetClassAirplanes, "airplane-red-small"), 136 | loader.MustFindTile(TilesetClassAirplanes, "airplane-red-medium"), 137 | loader.MustFindTile(TilesetClassAirplanes, "airplane-red-big"), 138 | } 139 | AirplanesGreen = []*ebiten.Image{ 140 | loader.MustFindTile(TilesetClassAirplanes, "airplane-green-small"), 141 | loader.MustFindTile(TilesetClassAirplanes, "airplane-green-medium"), 142 | loader.MustFindTile(TilesetClassAirplanes, "airplane-green-big"), 143 | } 144 | AirplanesYellow = []*ebiten.Image{ 145 | loader.MustFindTile(TilesetClassAirplanes, "airplane-yellow-small"), 146 | loader.MustFindTile(TilesetClassAirplanes, "airplane-yellow-medium"), 147 | loader.MustFindTile(TilesetClassAirplanes, "airplane-yellow-big"), 148 | } 149 | 150 | AirplaneGraySmall = loader.MustFindTile(TilesetClassAirplanes, "airplane-gray-small-2") 151 | 152 | TankBase = loader.MustFindTile(TilesetClassTiles, "tank-base") 153 | TankGun = loader.MustFindTile(TilesetClassTiles, "tank-gun") 154 | 155 | TurretBase = loader.MustFindTile(TilesetClassTiles, "turret-base") 156 | TurretGunSingle = loader.MustFindTile(TilesetClassTiles, "turret-gun-single") 157 | TurretGunDouble = loader.MustFindTile(TilesetClassTiles, "turret-gun-double") 158 | 159 | LaserSingle = loader.MustFindTile(TilesetClassTiles, "laser-single") 160 | Bullet = loader.MustFindTile(TilesetClassTiles, "bullet") 161 | Missile = loader.MustFindTile(TilesetClassTiles, "missile") 162 | 163 | Health = loader.MustFindTile(TilesetClassTiles, "health") 164 | WeaponUpgrade = loader.MustFindTile(TilesetClassTiles, "weapon-upgrade") 165 | Shield = loader.MustFindTile(TilesetClassTiles, "shield") 166 | 167 | AirplaneShield = mustNewEbitenImage(airplaneShieldData) 168 | Crosshair = loader.MustFindTile(TilesetClassTiles, "crosshair") 169 | 170 | JoystickBase = mustNewEbitenImage(joystickBaseData) 171 | JoystickKnob = mustNewEbitenImage(joystickKnobData) 172 | } 173 | 174 | func mustLoadFont(data []byte, size int) font.Face { 175 | f, err := opentype.Parse(data) 176 | if err != nil { 177 | panic(err) 178 | } 179 | 180 | face, err := opentype.NewFace(f, &opentype.FaceOptions{ 181 | Size: float64(size), 182 | DPI: 72, 183 | Hinting: font.HintingFull, 184 | }) 185 | if err != nil { 186 | panic(err) 187 | } 188 | 189 | return face 190 | } 191 | 192 | func mustNewEbitenImage(data []byte) *ebiten.Image { 193 | img, _, err := image.Decode(bytes.NewReader(data)) 194 | if err != nil { 195 | panic(err) 196 | } 197 | 198 | return ebiten.NewImageFromImage(img) 199 | } 200 | 201 | type levelLoader struct { 202 | Tilesets map[string]*tiled.Tileset 203 | } 204 | 205 | func newLevelLoader() *levelLoader { 206 | return &levelLoader{ 207 | Tilesets: make(map[string]*tiled.Tileset), 208 | } 209 | } 210 | 211 | func (l *levelLoader) MustLoadLevels() []Level { 212 | levelPaths, err := fs.Glob(assetsFS, "levels/level*.tmx") 213 | if err != nil { 214 | panic(err) 215 | } 216 | 217 | var levels []Level 218 | for _, path := range levelPaths { 219 | levels = append(levels, l.MustLoadLevel(path)) 220 | } 221 | 222 | return levels 223 | } 224 | 225 | func (l *levelLoader) MustLoadLevel(levelPath string) Level { 226 | levelMap, err := tiled.LoadFile(levelPath, tiled.WithFileSystem(assetsFS)) 227 | if err != nil { 228 | panic(err) 229 | } 230 | 231 | level := Level{} 232 | 233 | paths := map[uint32]Path{} 234 | for _, og := range levelMap.ObjectGroups { 235 | for _, o := range og.Objects { 236 | if len(o.PolyLines) > 0 { 237 | var points []math.Vec2 238 | for _, p := range o.PolyLines { 239 | for _, pp := range *p.Points { 240 | points = append(points, math.Vec2{ 241 | X: o.X + pp.X, 242 | Y: o.Y + pp.Y, 243 | }) 244 | } 245 | } 246 | paths[o.ID] = Path{ 247 | Loops: false, 248 | Points: points, 249 | } 250 | } 251 | if len(o.Polygons) > 0 { 252 | var points []math.Vec2 253 | for _, p := range o.Polygons { 254 | for _, pp := range *p.Points { 255 | points = append(points, math.Vec2{ 256 | X: o.X + pp.X, 257 | Y: o.Y + pp.Y, 258 | }) 259 | } 260 | } 261 | paths[o.ID] = Path{ 262 | Loops: true, 263 | Points: points, 264 | } 265 | } 266 | } 267 | } 268 | 269 | groupSpawns := map[uint32]EnemyGroupSpawn{} 270 | 271 | for _, og := range levelMap.ObjectGroups { 272 | for _, o := range og.Objects { 273 | if o.Class == ObjectClassGroupSpawn { 274 | groupSpawns[o.ID] = EnemyGroupSpawn{ 275 | Position: math.Vec2{ 276 | X: o.X, 277 | Y: o.Y, 278 | }, 279 | } 280 | } 281 | } 282 | } 283 | 284 | for _, og := range levelMap.ObjectGroups { 285 | for _, o := range og.Objects { 286 | if o.Class == EnemyClassAirplane || 287 | o.Class == EnemyClassTank || 288 | o.Class == EnemyClassTurretBeam || 289 | o.Class == EnemyClassTurretMissiles { 290 | enemy := Enemy{ 291 | Class: o.Class, 292 | Position: math.Vec2{ 293 | X: o.X, 294 | Y: o.Y, 295 | }, 296 | Rotation: o.Rotation, 297 | Speed: 1, 298 | } 299 | 300 | var groupSpawnID uint32 301 | for _, p := range o.Properties { 302 | if p.Name == "path" { 303 | pathID, err := strconv.Atoi(p.Value) 304 | if err != nil { 305 | panic("invalid path id: " + err.Error()) 306 | } 307 | 308 | path, ok := paths[uint32(pathID)] 309 | if !ok { 310 | panic("path not found: " + p.Value) 311 | } 312 | 313 | enemy.Path = path 314 | } 315 | if p.Name == "speed" { 316 | speed, err := strconv.ParseFloat(p.Value, 64) 317 | if err != nil { 318 | panic("invalid speed: " + err.Error()) 319 | } 320 | 321 | enemy.Speed = speed 322 | } 323 | if p.Name == "spawn" { 324 | groupSpawnIDInt, err := strconv.Atoi(p.Value) 325 | if err != nil { 326 | panic("invalid path id: " + err.Error()) 327 | } 328 | groupSpawnID = uint32(groupSpawnIDInt) 329 | } 330 | } 331 | 332 | if groupSpawnID == 0 { 333 | level.Enemies = append(level.Enemies, enemy) 334 | } else { 335 | groupSpawn, ok := groupSpawns[groupSpawnID] 336 | if !ok { 337 | panic(fmt.Sprintf("group spawn not found: %v", groupSpawnID)) 338 | } 339 | 340 | groupSpawn.Enemies = append(groupSpawn.Enemies, enemy) 341 | groupSpawns[groupSpawnID] = groupSpawn 342 | } 343 | } 344 | } 345 | } 346 | 347 | for _, groupSpawn := range groupSpawns { 348 | level.EnemyGroupSpawns = append(level.EnemyGroupSpawns, groupSpawn) 349 | } 350 | 351 | renderer, err := render.NewRendererWithFileSystem(levelMap, assetsFS) 352 | if err != nil { 353 | panic(err) 354 | } 355 | 356 | err = renderer.RenderLayer(0) 357 | if err != nil { 358 | panic(err) 359 | } 360 | 361 | level.Background = ebiten.NewImageFromImage(renderer.Result) 362 | 363 | for _, ts := range levelMap.Tilesets { 364 | if _, ok := l.Tilesets[ts.Class]; !ok { 365 | l.Tilesets[ts.Class] = ts 366 | } 367 | } 368 | 369 | return level 370 | } 371 | 372 | func (l *levelLoader) MustLoadAirBaseLevel() AirBaseLevel { 373 | levelMap, err := tiled.LoadFile("levels/airbase.tmx", tiled.WithFileSystem(assetsFS)) 374 | if err != nil { 375 | panic(err) 376 | } 377 | 378 | level := AirBaseLevel{} 379 | 380 | for _, og := range levelMap.ObjectGroups { 381 | for _, o := range og.Objects { 382 | if o.Class == "spawn" { 383 | spawn := Spawn{ 384 | Faction: o.Name, 385 | Position: math.Vec2{ 386 | X: o.X, 387 | Y: o.Y, 388 | }, 389 | } 390 | 391 | level.Spawns = append(level.Spawns, spawn) 392 | } 393 | } 394 | } 395 | 396 | renderer, err := render.NewRendererWithFileSystem(levelMap, assetsFS) 397 | if err != nil { 398 | panic(err) 399 | } 400 | 401 | err = renderer.RenderLayer(0) 402 | if err != nil { 403 | panic(err) 404 | } 405 | 406 | level.Background = ebiten.NewImageFromImage(renderer.Result) 407 | 408 | return level 409 | } 410 | 411 | func (l *levelLoader) MustFindTile(tilesetClass string, tileClass string) *ebiten.Image { 412 | ts, ok := l.Tilesets[tilesetClass] 413 | if !ok { 414 | panic(fmt.Sprintf("tileset not found: %s", tilesetClass)) 415 | } 416 | 417 | for _, t := range ts.Tiles { 418 | f, err := fs.ReadFile(assetsFS, filepath.Join("levels", ts.Image.Source)) 419 | if err != nil { 420 | panic(err) 421 | } 422 | 423 | tilesetImage := mustNewEbitenImage(f) 424 | if t.Class == tileClass { 425 | width := ts.TileWidth 426 | height := ts.TileHeight 427 | 428 | col := int(t.ID) % ts.Columns 429 | row := int(t.ID) / ts.Columns 430 | 431 | // Plus one because of 1px margin 432 | if col > 0 { 433 | width += 1 434 | } 435 | if row > 0 { 436 | height += 1 437 | } 438 | 439 | sx := col * width 440 | sy := row * height 441 | 442 | return tilesetImage.SubImage( 443 | image.Rect(sx, sy, sx+ts.TileWidth, sy+ts.TileHeight), 444 | ).(*ebiten.Image) 445 | } 446 | } 447 | 448 | panic(fmt.Sprintf("tile not found: %s", tileClass)) 449 | } 450 | -------------------------------------------------------------------------------- /assets/fonts/kenney-future-narrow.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/fonts/kenney-future-narrow.ttf -------------------------------------------------------------------------------- /assets/fonts/kenney-future.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/fonts/kenney-future.ttf -------------------------------------------------------------------------------- /assets/levels/airbase.tmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 8 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 9 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,65,43,43,43,43,43, 10 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 11 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 12 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 13 | 43,43,43,43,43,65,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 14 | 43,43,43,43,43,66,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 15 | 43,43,43,43,43,43,43,43,66,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,66,43,43,43,43, 16 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 17 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,38,39,39,39,40,43, 18 | 43,43,43,43,38,40,43,38,39,39,39,40,43,43,43,43,38,40,43,43,43,43,43,43,50,51,51,51,52,43, 19 | 43,43,43,38,54,53,39,54,51,51,51,53,39,39,39,39,54,52,43,65,43,43,43,43,50,51,51,51,52,43, 20 | 43,43,38,54,51,51,51,51,51,51,51,51,51,51,51,51,51,52,66,43,43,43,43,43,50,51,51,51,52,43, 21 | 43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,51,51,53,40,43,43,43,43,43,50,51,51,51,52,43, 22 | 39,39,54,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,53,39,39,39,39,39,54,51,51,51,53,39, 23 | 111,61,111,111,111,61,111,111,37,111,61,111,111,111,37,111,111,111,111,61,111,111,111,111,111,111,61,111,111,111, 24 | 111,61,49,37,49,111,61,37,49,111,37,111,49,61,111,49,61,111,61,111,37,49,61,61,61,49,37,49,111,61, 25 | 37,111,111,61,37,111,61,111,61,111,61,49,111,61,111,49,37,111,49,61,111,61,49,37,49,111,61,111,61,111, 26 | 111,111,111,61,111,49,111,111,111,49,111,111,37,111,49,111,111,111,37,111,111,111,111,111,61,111,111,49,111,111, 27 | 111,111,111,111,111,111,111,101,111,111,111,111,101,111,111,111,111,101,111,111,111,111,101,111,111,111,111,111,111,111, 28 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 29 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 30 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 31 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 32 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 33 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 34 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 35 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,51,51,86,111,111,111,111,111,111,111, 36 | 111,111,111,111,111,111,51,86,111,111,111,111,86,111,111,51,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 37 | 111,111,111,111,111,51,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 38 | 111,111,111,111,111,51,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 39 | 111,111,111,111,51,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 40 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 41 | 111,111,111,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,86,111,111,111,111,111,111,111, 42 | 51,51,51,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,51,51,51, 43 | 51,51,51,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,51,51,51, 44 | 51,51,51,51,51,51,51,86,51,51,51,51,86,111,51,51,51,86,51,51,51,51,86,51,51,51,51,51,51,51, 45 | 51,51,51,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,51,51,51, 46 | 51,51,51,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,86,51,51,51,51,51,51,51 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /assets/levels/airplanes.tiled-project: -------------------------------------------------------------------------------- 1 | { 2 | "automappingRulesFile": "", 3 | "commands": [ 4 | ], 5 | "extensionsPath": "extensions", 6 | "folders": [ 7 | "." 8 | ], 9 | "propertyTypes": [ 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /assets/levels/airplanes.tiled-session: -------------------------------------------------------------------------------- 1 | { 2 | "Map/SizeTest": { 3 | "height": 4300, 4 | "width": 2 5 | }, 6 | "activeFile": "", 7 | "expandedProjectPaths": [ 8 | "." 9 | ], 10 | "fileStates": { 11 | ":/automap-tiles.tsx": { 12 | "scaleInDock": 1 13 | }, 14 | "airbase.tmx": { 15 | "expandedObjectLayers": [ 16 | 2 17 | ], 18 | "scale": 0.4932, 19 | "selectedLayer": 0, 20 | "viewCenter": { 21 | "x": 462.287104622871, 22 | "y": 288.9294403892944 23 | } 24 | }, 25 | "airplanes.tsx": { 26 | "scaleInDock": 1, 27 | "scaleInEditor": 2.14 28 | }, 29 | "level01.tmx": { 30 | "expandedObjectLayers": [ 31 | 3, 32 | 2 33 | ], 34 | "scale": 1.1848, 35 | "selectedLayer": 2, 36 | "viewCenter": { 37 | "x": 275.151924375422, 38 | "y": 266.2896691424713 39 | } 40 | }, 41 | "level02.tmx": { 42 | "expandedObjectLayers": [ 43 | 3, 44 | 2 45 | ], 46 | "scale": 0.5587, 47 | "selectedLayer": 1, 48 | "viewCenter": { 49 | "x": 526.2215858242349, 50 | "y": 1652.9443350635404 51 | } 52 | }, 53 | "level03.tmx": { 54 | "expandedObjectLayers": [ 55 | 3 56 | ], 57 | "scale": 0.4562, 58 | "selectedLayer": 1, 59 | "viewCenter": { 60 | "x": 282.7707145988601, 61 | "y": 327.7071459886016 62 | } 63 | }, 64 | "level1.tmx": { 65 | "expandedObjectLayers": [ 66 | 3, 67 | 2 68 | ], 69 | "scale": 0.5, 70 | "selectedLayer": 2, 71 | "viewCenter": { 72 | "x": 248, 73 | "y": 1528 74 | } 75 | }, 76 | "level2.tmx": { 77 | "expandedObjectLayers": [ 78 | 2 79 | ], 80 | "scale": 1.36, 81 | "selectedLayer": 1, 82 | "viewCenter": { 83 | "x": 228.67647058823528, 84 | "y": 1398.5294117647059 85 | } 86 | }, 87 | "ships.tsx": { 88 | "scaleInDock": 1, 89 | "scaleInEditor": 2.14 90 | }, 91 | "tiles.tsx": { 92 | "scaleInDock": 2.4, 93 | "scaleInEditor": 2.14 94 | } 95 | }, 96 | "last.imagePath": "/Users/milosz/src/gamedev/airplanes/assets/airplanes", 97 | "map.height": 80, 98 | "map.lastUsedFormat": "tmx", 99 | "openFiles": [ 100 | ], 101 | "project": "airplanes.tiled-project", 102 | "property.type": "float", 103 | "recentFiles": [ 104 | "level03.tmx", 105 | "level01.tmx", 106 | "level2.tmx", 107 | "level1.tmx", 108 | "tiles.tsx", 109 | "ships.tsx" 110 | ], 111 | "resizeMap.removeObjects": false, 112 | "tileset.lastUsedFormat": "tsx", 113 | "tileset.spacing": 1, 114 | "tileset.tileSize": { 115 | "height": 32, 116 | "width": 32 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /assets/levels/airplanes.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /assets/levels/playground.tmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 8 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 9 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 10 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 11 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 12 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 13 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 14 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 15 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 16 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 17 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 18 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 19 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 20 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 21 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 22 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 23 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 24 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 25 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 26 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 27 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 28 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 29 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,38,39,39,39,39,39,39,39,39,39,39,39,39,40,43, 30 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43, 31 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43, 32 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43, 33 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,41,63,63,42,51,51,51,51,51,52,43, 34 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,52,43,43,50,51,51,51,51,51,52,43, 35 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,52,43,43,50,51,51,51,51,51,52,43, 36 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,52,43,43,50,51,51,51,51,51,52,43, 37 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,53,39,39,54,51,51,112,51,51,52,43, 38 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43, 39 | 43,43,43,43,43,65,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43, 40 | 43,43,43,43,43,43,65,43,43,43,43,43,43,43,43,50,51,112,51,51,51,51,51,112,51,51,51,51,52,43, 41 | 43,43,43,43,43,65,43,66,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,112,52,43, 42 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43, 43 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,62,63,63,63,63,63,63,63,63,63,63,63,63,64,43, 44 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 45 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 46 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 47 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 48 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 49 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 50 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 51 | 43,43,43,38,39,39,39,39,39,39,39,39,39,39,39,39,40,43,43,43,43,43,43,43,43,43,43,43,43,43, 52 | 43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 53 | 43,43,43,50,51,51,51,51,51,51,51,51,112,51,51,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 54 | 43,43,43,50,51,51,51,51,85,51,51,51,51,51,51,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 55 | 43,43,43,50,51,51,51,51,88,51,51,51,112,51,112,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 56 | 43,43,43,50,51,51,51,51,88,51,51,51,51,51,51,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 57 | 43,43,43,50,51,51,51,51,88,51,51,51,51,51,51,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 58 | 43,43,43,50,51,51,51,51,88,51,51,51,51,51,51,51,52,43,43,43,43,43,43,43,43,43,43,43,43,43, 59 | 43,43,43,50,51,51,51,51,51,51,51,51,51,51,51,51,52,43,43,38,39,39,39,39,39,39,39,39,39,40, 60 | 43,43,43,62,63,63,63,63,63,63,63,63,63,63,63,63,64,43,43,50,51,51,51,51,51,51,51,51,51,52, 61 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,112,51,51,52, 62 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,112,51,51,51,51,51,52, 63 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,51,51,51,52, 64 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,50,51,51,51,51,51,51,112,51,51,52, 65 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,62,63,63,63,63,63,63,63,63,63,64, 66 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 67 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 68 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 69 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,73,51,51,51,73,51,51,51,51,51,51,51,51,51,51,51, 70 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 71 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 72 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 73 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 74 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 75 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 76 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 77 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 78 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 79 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 80 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 81 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 82 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 83 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 84 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 85 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 86 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,73,51, 87 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 88 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 89 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 90 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 91 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 92 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 93 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,103,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 94 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 95 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 96 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 97 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 98 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 99 | 57,57,57,57,57,57,57,57,57,57,57,57,57,103,57,57,57,103,57,57,57,57,57,57,57,57,57,57,57,57, 100 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 101 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 102 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 103 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,57,57,57,51,51,51,51,51,51, 104 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,57,57,57,57,57,57,57,51,51,51,51, 105 | 51,74,75,75,75,75,75,75,75,89,114,51,51,51,51,51,51,51,51,57,57,57,57,57,57,51,51,51,51,51, 106 | 51,86,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,57,57,57,57,57,57,57,51,51,51,51,51,51, 107 | 51,86,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,57,57,57,57,57,57,51,51,51,51,51,51,51, 108 | 51,86,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,57,57,51,51,51,51,51,51,51,51,51, 109 | 51,86,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 110 | 75,90,75,75,75,75,75,75,75,100,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 111 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 112 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 113 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 114 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 115 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 116 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 117 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 118 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 119 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 120 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 121 | 43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, 122 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 123 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 124 | 75,75,75,75,75,75,75,75,76,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 125 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 126 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 127 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 128 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,97,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 129 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 130 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 131 | 51,51,51,51,51,51,51,51,88,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 132 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 133 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 134 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 135 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 136 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 137 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 138 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 139 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 140 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 141 | 57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57, 142 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 143 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 144 | 51,51,51,51,51,51,51,51,51,51,51,51,51,97,51,51,51,97,51,51,51,51,51,51,51,51,51,51,51,51, 145 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51, 146 | 51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /assets/levels/tiles.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/tiles/airplane_shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/tiles/airplane_shield.png -------------------------------------------------------------------------------- /assets/tiles/joystick-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/tiles/joystick-base.png -------------------------------------------------------------------------------- /assets/tiles/joystick-knob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/tiles/joystick-knob.png -------------------------------------------------------------------------------- /assets/tiles/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/assets/tiles/tiles.png -------------------------------------------------------------------------------- /component/ai.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | ) 7 | 8 | const ( 9 | AITypeConstantVelocity AIType = iota 10 | AITypeFollowPath 11 | ) 12 | 13 | type AIType int 14 | 15 | type AIData struct { 16 | Type AIType 17 | 18 | Speed float64 19 | 20 | StartedMoving bool 21 | 22 | Path []math.Vec2 23 | PathLoops bool 24 | NextTarget int 25 | } 26 | 27 | var AI = donburi.NewComponentType[AIData]() 28 | -------------------------------------------------------------------------------- /component/altitude.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | type AltitudeData struct { 10 | // Altitude is the level above the ground, in percent. 11 | // 0.0 is ground level, 1.0 is the highest point. 12 | Altitude float64 13 | Velocity float64 14 | 15 | // TODO: Not sure if this fits this component 16 | Falling bool 17 | } 18 | 19 | func (a *AltitudeData) Update() { 20 | a.Altitude = engine.Clamp(a.Altitude+a.Velocity, 0, 1) 21 | } 22 | 23 | var Altitude = donburi.NewComponentType[AltitudeData]() 24 | -------------------------------------------------------------------------------- /component/bounds.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type BoundsData struct { 6 | Disabled bool 7 | } 8 | 9 | // Bounds indicates that the entity can't move of out the screen. 10 | var Bounds = donburi.NewComponentType[BoundsData]() 11 | -------------------------------------------------------------------------------- /component/camera.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | type CameraData struct { 10 | Moving bool 11 | MoveTimer *engine.Timer 12 | } 13 | 14 | var Camera = donburi.NewComponentType[CameraData]() 15 | -------------------------------------------------------------------------------- /component/collectible.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type CollectibleType int 6 | 7 | const ( 8 | CollectibleTypeHealth CollectibleType = iota 9 | CollectibleTypeWeaponUpgrade 10 | CollectibleTypeShield 11 | ) 12 | 13 | type CollectibleData struct { 14 | Type CollectibleType 15 | } 16 | 17 | var Collectible = donburi.NewComponentType[CollectibleData]() 18 | -------------------------------------------------------------------------------- /component/collider.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | ) 6 | 7 | const ( 8 | CollisionLayerPlayerBullets ColliderLayer = iota 9 | CollisionLayerEnemyBullets 10 | CollisionLayerGroundEnemies 11 | CollisionLayerAirEnemies 12 | CollisionLayerPlayers 13 | CollisionLayerCollectibles 14 | ) 15 | 16 | type ColliderLayer int 17 | 18 | type ColliderData struct { 19 | Width float64 20 | Height float64 21 | Layer ColliderLayer 22 | } 23 | 24 | var Collider = donburi.NewComponentType[ColliderData]() 25 | -------------------------------------------------------------------------------- /component/debug.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type DebugData struct { 6 | Enabled bool 7 | } 8 | 9 | var Debug = donburi.NewComponentType[DebugData]() 10 | -------------------------------------------------------------------------------- /component/despawnable.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type DespawnableData struct { 6 | // Set when the unit first appears on-screen 7 | Spawned bool 8 | } 9 | 10 | var Despawnable = donburi.NewComponentType[DespawnableData]() 11 | -------------------------------------------------------------------------------- /component/destroyed.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type DestroyedData struct{} 6 | 7 | var Destroyed = donburi.NewComponentType[DestroyedData]() 8 | 9 | func Destroy(e *donburi.Entry) { 10 | if !e.Valid() { 11 | return 12 | } 13 | if !e.HasComponent(Destroyed) { 14 | e.AddComponent(Destroyed) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /component/distancelimit.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | ) 7 | 8 | type DistanceLimitData struct { 9 | MaxDistance float64 10 | 11 | DistanceTraveled float64 12 | PreviousPosition math.Vec2 13 | Initialized bool 14 | } 15 | 16 | var DistanceLimit = donburi.NewComponentType[DistanceLimitData]() 17 | -------------------------------------------------------------------------------- /component/evolution.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | const maxLevel = 2 10 | 11 | type EvolutionData struct { 12 | Level int 13 | Evolving bool 14 | StartedEvolving bool 15 | GrowTimer *engine.Timer 16 | ShrinkTimer *engine.Timer 17 | } 18 | 19 | func (e *EvolutionData) Evolve() { 20 | if e.Level >= maxLevel { 21 | return 22 | } 23 | 24 | e.Level++ 25 | e.GrowTimer.Reset() 26 | e.ShrinkTimer.Reset() 27 | e.Evolving = true 28 | e.StartedEvolving = false 29 | } 30 | 31 | func (e *EvolutionData) StopEvolving() { 32 | e.Evolving = false 33 | } 34 | 35 | var Evolution = donburi.NewComponentType[EvolutionData]() 36 | -------------------------------------------------------------------------------- /component/follower.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | type FollowerData struct { 10 | Target *donburi.Entry 11 | FollowingSpeed float64 12 | FollowingTimer *engine.Timer 13 | } 14 | 15 | var Follower = donburi.NewComponentType[FollowerData]() 16 | -------------------------------------------------------------------------------- /component/game.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/filter" 6 | ) 7 | 8 | type GameData struct { 9 | Score int 10 | Paused bool 11 | GameOver bool 12 | Settings Settings 13 | } 14 | 15 | func (d *GameData) AddScore(score int) { 16 | d.Score += score 17 | } 18 | 19 | type Settings struct { 20 | ScreenWidth int 21 | ScreenHeight int 22 | } 23 | 24 | var Game = donburi.NewComponentType[GameData]() 25 | 26 | func MustFindGame(w donburi.World) *GameData { 27 | game, ok := donburi.NewQuery(filter.Contains(Game)).First(w) 28 | if !ok { 29 | panic("game not found") 30 | } 31 | return Game.Get(game) 32 | } 33 | -------------------------------------------------------------------------------- /component/health.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/yohamta/donburi" 7 | 8 | "github.com/m110/airplanes/engine" 9 | ) 10 | 11 | type HealthData struct { 12 | Health int 13 | JustDamaged bool 14 | DamageIndicatorTimer *engine.Timer 15 | DamageIndicator *SpriteData 16 | } 17 | 18 | func (d *HealthData) Damage() { 19 | if d.Health <= 0 { 20 | return 21 | } 22 | 23 | d.Health-- 24 | d.JustDamaged = true 25 | d.DamageIndicatorTimer.Reset() 26 | d.DamageIndicator.Hidden = false 27 | } 28 | 29 | func (d *HealthData) HideDamageIndicator() { 30 | d.JustDamaged = false 31 | d.DamageIndicator.Hidden = true 32 | } 33 | 34 | var Health = donburi.NewComponentType[HealthData](HealthData{ 35 | DamageIndicatorTimer: engine.NewTimer(time.Millisecond * 100), 36 | }) 37 | -------------------------------------------------------------------------------- /component/input.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/yohamta/donburi" 6 | ) 7 | 8 | type InputData struct { 9 | Disabled bool 10 | 11 | MoveUpKey ebiten.Key 12 | MoveRightKey ebiten.Key 13 | MoveDownKey ebiten.Key 14 | MoveLeftKey ebiten.Key 15 | MoveSpeed float64 16 | 17 | ShootKey ebiten.Key 18 | } 19 | 20 | var Input = donburi.NewComponentType[InputData]() 21 | -------------------------------------------------------------------------------- /component/joystick.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type JoystickData struct { 6 | } 7 | 8 | var Joystick = donburi.NewComponentType[JoystickData]() 9 | -------------------------------------------------------------------------------- /component/label.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type LabelData struct { 6 | Text string 7 | Hidden bool 8 | } 9 | 10 | var Label = donburi.NewComponentType[LabelData]() 11 | -------------------------------------------------------------------------------- /component/level.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/filter" 6 | 7 | "github.com/m110/airplanes/engine" 8 | ) 9 | 10 | type LevelData struct { 11 | ProgressionTimer *engine.Timer 12 | ReachedEnd bool 13 | Progressed bool 14 | } 15 | 16 | var Level = donburi.NewComponentType[LevelData]() 17 | 18 | func MustFindLevel(w donburi.World) *donburi.Entry { 19 | level, ok := donburi.NewQuery(filter.Contains(Level)).First(w) 20 | if !ok { 21 | panic("no level found") 22 | } 23 | 24 | return level 25 | } 26 | -------------------------------------------------------------------------------- /component/observer.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | ) 7 | 8 | type ObserverData struct { 9 | LookFor *donburi.Query 10 | Target *donburi.Entry 11 | } 12 | 13 | var Observer = donburi.NewComponentType[ObserverData]() 14 | 15 | func ClosestTarget(w donburi.World, entry *donburi.Entry, lookFor *donburi.Query) *donburi.Entry { 16 | pos := transform.WorldPosition(entry) 17 | 18 | var closestDistance float64 19 | var closestTarget *donburi.Entry 20 | lookFor.Each(w, func(target *donburi.Entry) { 21 | targetPos := transform.WorldPosition(target) 22 | distance := pos.Distance(targetPos) 23 | 24 | if closestTarget == nil || distance < closestDistance { 25 | closestTarget = target 26 | closestDistance = distance 27 | } 28 | }) 29 | 30 | return closestTarget 31 | } 32 | -------------------------------------------------------------------------------- /component/player.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/yohamta/donburi" 8 | 9 | "github.com/m110/airplanes/engine" 10 | ) 11 | 12 | type PlayerFaction int 13 | 14 | const ( 15 | PlayerFactionBlue PlayerFaction = iota 16 | PlayerFactionRed 17 | PlayerFactionGreen 18 | PlayerFactionYellow 19 | ) 20 | 21 | func MustPlayerFactionFromString(s string) PlayerFaction { 22 | switch s { 23 | case "blue": 24 | return PlayerFactionBlue 25 | case "red": 26 | return PlayerFactionRed 27 | case "green": 28 | return PlayerFactionGreen 29 | case "yellow": 30 | return PlayerFactionYellow 31 | default: 32 | panic(fmt.Sprintf("unknown player faction: %v", s)) 33 | } 34 | } 35 | 36 | type WeaponLevel int 37 | 38 | const ( 39 | WeaponLevelSingle WeaponLevel = iota 40 | WeaponLevelSingleFast 41 | WeaponLevelDouble 42 | WeaponLevelDoubleFast 43 | WeaponLevelDiagonal 44 | WeaponLevelDoubleDiagonal 45 | ) 46 | 47 | type PlayerData struct { 48 | PlayerNumber int 49 | PlayerFaction PlayerFaction 50 | 51 | Lives int 52 | Respawning bool 53 | RespawnTimer *engine.Timer 54 | 55 | WeaponLevel WeaponLevel 56 | ShootTimer *engine.Timer 57 | } 58 | 59 | func (d *PlayerData) AddLive() { 60 | d.Lives++ 61 | } 62 | 63 | func (d *PlayerData) Damage() { 64 | if d.Respawning { 65 | return 66 | } 67 | 68 | d.Lives-- 69 | 70 | if d.Lives > 0 { 71 | d.Respawning = true 72 | d.RespawnTimer.Reset() 73 | } 74 | } 75 | 76 | func (d *PlayerData) UpgradeWeapon() { 77 | if d.WeaponLevel == WeaponLevelDoubleDiagonal { 78 | return 79 | } 80 | d.WeaponLevel++ 81 | d.ShootTimer = engine.NewTimer(d.WeaponCooldown()) 82 | } 83 | 84 | func (d *PlayerData) WeaponCooldown() time.Duration { 85 | switch d.WeaponLevel { 86 | case WeaponLevelSingle: 87 | return 400 * time.Millisecond 88 | case WeaponLevelSingleFast: 89 | return 300 * time.Millisecond 90 | case WeaponLevelDouble: 91 | return 300 * time.Millisecond 92 | case WeaponLevelDoubleFast: 93 | return 200 * time.Millisecond 94 | case WeaponLevelDiagonal: 95 | return 200 * time.Millisecond 96 | case WeaponLevelDoubleDiagonal: 97 | return 200 * time.Millisecond 98 | default: 99 | panic(fmt.Sprintf("unknown weapon level: %v", d.WeaponLevel)) 100 | } 101 | } 102 | 103 | func (d *PlayerData) EvolutionLevel() int { 104 | switch d.WeaponLevel { 105 | case WeaponLevelSingle: 106 | fallthrough 107 | case WeaponLevelSingleFast: 108 | return 0 109 | case WeaponLevelDouble: 110 | fallthrough 111 | case WeaponLevelDoubleFast: 112 | return 1 113 | case WeaponLevelDiagonal: 114 | fallthrough 115 | case WeaponLevelDoubleDiagonal: 116 | return 2 117 | default: 118 | panic(fmt.Sprintf("unknown weapon level: %v", d.WeaponLevel)) 119 | } 120 | } 121 | 122 | var Player = donburi.NewComponentType[PlayerData]() 123 | -------------------------------------------------------------------------------- /component/playerairplane.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | type PlayerAirplaneData struct { 10 | PlayerNumber int 11 | // TODO Duplicated across PlayerAirplane and Player? 12 | Faction PlayerFaction 13 | 14 | Invulnerable bool 15 | InvulnerableTimer *engine.Timer 16 | InvulnerableIndicator *SpriteData 17 | } 18 | 19 | func (d *PlayerAirplaneData) StartInvulnerability() { 20 | d.Invulnerable = true 21 | d.InvulnerableTimer.Reset() 22 | d.InvulnerableIndicator.Hidden = false 23 | } 24 | 25 | func (d *PlayerAirplaneData) StopInvulnerability() { 26 | d.Invulnerable = false 27 | d.InvulnerableIndicator.Hidden = true 28 | } 29 | 30 | var PlayerAirplane = donburi.NewComponentType[PlayerAirplaneData]() 31 | -------------------------------------------------------------------------------- /component/playerselect.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type PlayerSelectData struct { 6 | Index int 7 | Faction PlayerFaction 8 | 9 | Selected bool 10 | Ready bool 11 | PlayerNumber int 12 | } 13 | 14 | func (p *PlayerSelectData) Select(playerNumber int) { 15 | p.Selected = true 16 | p.PlayerNumber = playerNumber 17 | } 18 | 19 | func (p *PlayerSelectData) Unselect() { 20 | p.Selected = false 21 | p.PlayerNumber = 0 22 | } 23 | 24 | func (p *PlayerSelectData) LockIn() { 25 | p.Ready = true 26 | } 27 | 28 | func (p *PlayerSelectData) Release() { 29 | p.Ready = false 30 | } 31 | 32 | var PlayerSelect = donburi.NewComponentType[PlayerSelectData]() 33 | -------------------------------------------------------------------------------- /component/script.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type ScriptData struct { 6 | Update func(w donburi.World) 7 | } 8 | 9 | var Script = donburi.NewComponentType[ScriptData]() 10 | -------------------------------------------------------------------------------- /component/shooter.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | type ShooterType int 10 | 11 | const ( 12 | ShooterTypeBullet ShooterType = iota 13 | ShooterTypeMissile 14 | ShooterTypeBeam 15 | ) 16 | 17 | type ShooterData struct { 18 | Type ShooterType 19 | ShootTimer *engine.Timer 20 | } 21 | 22 | var Shooter = donburi.NewComponentType[ShooterData]() 23 | -------------------------------------------------------------------------------- /component/spawnable.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | type SpawnFunc func(w donburi.World) 6 | 7 | type SpawnableData struct { 8 | SpawnFunc SpawnFunc 9 | } 10 | 11 | var Spawnable = donburi.NewComponentType[SpawnableData]() 12 | -------------------------------------------------------------------------------- /component/sprite.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/yohamta/donburi" 6 | ) 7 | 8 | type SpriteLayer int 9 | 10 | const ( 11 | SpriteLayerBackground SpriteLayer = iota 12 | SpriteLayerDebris 13 | SpriteLayerGroundUnits 14 | SpriteLayerGroundGuns 15 | SpriteLayerShadows 16 | SpriteLayerCollectibles 17 | SpriteLayerFallingWrecks 18 | SpriteLayerAirUnits 19 | SpriteLayerIndicators 20 | SpriteLayerUI 21 | ) 22 | 23 | type SpritePivot int 24 | 25 | const ( 26 | SpritePivotCenter SpritePivot = iota 27 | SpritePivotTopLeft 28 | ) 29 | 30 | type SpriteData struct { 31 | Image *ebiten.Image 32 | Layer SpriteLayer 33 | Pivot SpritePivot 34 | 35 | // The original rotation of the sprite 36 | // "Facing right" is considered 0 degrees 37 | OriginalRotation float64 38 | 39 | Hidden bool 40 | 41 | ColorOverride *ColorOverride 42 | } 43 | 44 | type ColorOverride struct { 45 | R, G, B, A float64 46 | } 47 | 48 | func (s *SpriteData) Show() { 49 | s.Hidden = false 50 | } 51 | 52 | func (s *SpriteData) Hide() { 53 | s.Hidden = true 54 | } 55 | 56 | var Sprite = donburi.NewComponentType[SpriteData]() 57 | -------------------------------------------------------------------------------- /component/tags.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/yohamta/donburi" 4 | 5 | var ( 6 | UI = donburi.NewTag() 7 | 8 | ShadowTag = donburi.NewTag() 9 | 10 | CurrentEvolutionTag = donburi.NewTag() 11 | NextEvolutionTag = donburi.NewTag() 12 | 13 | Wreckable = donburi.NewTag() 14 | ) 15 | -------------------------------------------------------------------------------- /component/timetolive.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/engine" 7 | ) 8 | 9 | type TimeToLiveData struct { 10 | Timer *engine.Timer 11 | } 12 | 13 | var TimeToLive = donburi.NewComponentType[TimeToLiveData]() 14 | -------------------------------------------------------------------------------- /component/velocity.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | ) 7 | 8 | type VelocityData struct { 9 | Velocity math.Vec2 10 | RotationVelocity float64 11 | } 12 | 13 | var Velocity = donburi.NewComponentType[VelocityData]() 14 | -------------------------------------------------------------------------------- /docs/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/docs/debug.png -------------------------------------------------------------------------------- /docs/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/docs/editor.png -------------------------------------------------------------------------------- /docs/evolution.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/docs/evolution.gif -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/docs/screenshot.png -------------------------------------------------------------------------------- /engine/clamp.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | func Clamp(value, min, max float64) float64 { 4 | if value < min { 5 | return min 6 | } 7 | if value > max { 8 | return max 9 | } 10 | return value 11 | } 12 | -------------------------------------------------------------------------------- /engine/find.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yohamta/donburi" 6 | "github.com/yohamta/donburi/component" 7 | "github.com/yohamta/donburi/features/transform" 8 | "github.com/yohamta/donburi/filter" 9 | ) 10 | 11 | func MustGetParent(entry *donburi.Entry) *donburi.Entry { 12 | parent, ok := transform.GetParent(entry) 13 | if !ok { 14 | panic("parent not found") 15 | } 16 | return parent 17 | } 18 | 19 | func MustFindChildWithComponent(parent *donburi.Entry, componentType component.IComponentType) *donburi.Entry { 20 | entry, ok := transform.FindChildWithComponent(parent, componentType) 21 | if !ok { 22 | panic(fmt.Sprintf("entry not found with component %T", componentType)) 23 | } 24 | return entry 25 | } 26 | 27 | func FindWithComponent(w donburi.World, componentType component.IComponentType) (*donburi.Entry, bool) { 28 | return donburi.NewQuery(filter.Contains(componentType)).First(w) 29 | } 30 | 31 | func MustFindWithComponent(w donburi.World, componentType component.IComponentType) *donburi.Entry { 32 | entry, ok := FindWithComponent(w, componentType) 33 | if !ok { 34 | panic(fmt.Sprintf("entry not found with component %T", componentType)) 35 | } 36 | return entry 37 | } 38 | 39 | type Component[T any] interface { 40 | donburi.IComponentType 41 | Get(entry *donburi.Entry) *T 42 | } 43 | 44 | func MustFindComponent[T any](w donburi.World, c Component[T]) *T { 45 | entry, ok := donburi.NewQuery(filter.Contains(c)).First(w) 46 | if !ok { 47 | panic("MustFindComponent: entry not found") 48 | } 49 | 50 | return c.Get(entry) 51 | } 52 | -------------------------------------------------------------------------------- /engine/hierarchy.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/component" 6 | "github.com/yohamta/donburi/features/transform" 7 | ) 8 | 9 | func FindChildrenWithComponent(e *donburi.Entry, c component.IComponentType) []*donburi.Entry { 10 | if !e.Valid() { 11 | return nil 12 | } 13 | 14 | children, ok := transform.GetChildren(e) 15 | if !ok { 16 | return nil 17 | } 18 | 19 | var result []*donburi.Entry 20 | for _, child := range children { 21 | if !child.Valid() { 22 | continue 23 | } 24 | 25 | if child.HasComponent(c) { 26 | result = append(result, child) 27 | } 28 | 29 | result = append(result, FindChildrenWithComponent(child, c)...) 30 | } 31 | 32 | return result 33 | } 34 | -------------------------------------------------------------------------------- /engine/input/input.go: -------------------------------------------------------------------------------- 1 | package input 2 | -------------------------------------------------------------------------------- /engine/input/touch.go: -------------------------------------------------------------------------------- 1 | //go:build !android && !ios 2 | 3 | package input 4 | 5 | func IsTouchPrimaryInput() bool { 6 | return false 7 | } 8 | -------------------------------------------------------------------------------- /engine/input/touch_mobile.go: -------------------------------------------------------------------------------- 1 | //go:build android || ios 2 | 3 | package input 4 | 5 | func IsTouchPrimaryInput() bool { 6 | return true 7 | } 8 | -------------------------------------------------------------------------------- /engine/random.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "math/rand" 4 | 5 | func RandomFloatRange(min, max float64) float64 { 6 | return min + rand.Float64()*(max-min) 7 | } 8 | 9 | func RandomIntRange(min, max int) int { 10 | return min + rand.Intn(max-min+1) 11 | } 12 | 13 | func RandomFrom[T comparable](list []T) T { 14 | index := rand.Intn(len(list)) 15 | return list[index] 16 | } 17 | 18 | func RandomFromOrEmpty[T comparable](list []T) *T { 19 | index := rand.Intn(len(list) + 1) 20 | if index == len(list) { 21 | return nil 22 | } 23 | return &list[index] 24 | } 25 | -------------------------------------------------------------------------------- /engine/range.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "time" 4 | 5 | type IntRange struct { 6 | Min int 7 | Max int 8 | } 9 | 10 | func (r IntRange) Random() int { 11 | return RandomIntRange(r.Min, r.Max) 12 | } 13 | 14 | type FloatRange struct { 15 | Min float64 16 | Max float64 17 | } 18 | 19 | func (r FloatRange) Random() float64 { 20 | return RandomFloatRange(r.Min, r.Max) 21 | } 22 | 23 | type DurationRange struct { 24 | Min time.Duration 25 | Max time.Duration 26 | } 27 | 28 | func (r DurationRange) Random() time.Duration { 29 | return time.Duration(RandomIntRange(int(r.Min), int(r.Max))) 30 | } 31 | -------------------------------------------------------------------------------- /engine/rect.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "image" 4 | 5 | type Rect struct { 6 | X float64 7 | Y float64 8 | Width float64 9 | Height float64 10 | } 11 | 12 | func NewRect(x, y, width, height float64) Rect { 13 | return Rect{ 14 | X: x, 15 | Y: y, 16 | Width: width, 17 | Height: height, 18 | } 19 | } 20 | 21 | func (r Rect) MaxX() float64 { 22 | return r.X + r.Width 23 | } 24 | 25 | func (r Rect) MaxY() float64 { 26 | return r.Y + r.Height 27 | } 28 | 29 | func (r Rect) Intersects(other Rect) bool { 30 | return r.X <= other.MaxX() && 31 | other.X <= r.MaxX() && 32 | r.Y <= other.MaxY() && 33 | other.Y <= r.MaxY() 34 | } 35 | 36 | func (r Rect) ToImageRectangle() image.Rectangle { 37 | return image.Rect( 38 | int(r.X), 39 | int(r.Y), 40 | int(r.MaxX()), 41 | int(r.MaxY()), 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /engine/timer.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Timer struct { 8 | currentFrames int 9 | targetFrames int 10 | } 11 | 12 | func NewTimer(d time.Duration) *Timer { 13 | return &Timer{ 14 | currentFrames: 0, 15 | targetFrames: int(d.Milliseconds()) * 60 / 1000, 16 | } 17 | } 18 | 19 | func (t *Timer) Update() { 20 | if t.currentFrames < t.targetFrames { 21 | t.currentFrames++ 22 | } 23 | } 24 | 25 | func (t *Timer) IsReady() bool { 26 | return t.currentFrames >= t.targetFrames 27 | } 28 | 29 | func (t *Timer) Reset() { 30 | t.currentFrames = 0 31 | } 32 | 33 | func (t *Timer) PercentDone() float64 { 34 | return float64(t.currentFrames) / float64(t.targetFrames) 35 | } 36 | -------------------------------------------------------------------------------- /game/game.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | 6 | "github.com/m110/airplanes/assets" 7 | "github.com/m110/airplanes/scene" 8 | "github.com/m110/airplanes/system" 9 | ) 10 | 11 | type Scene interface { 12 | Update() 13 | Draw(screen *ebiten.Image) 14 | } 15 | 16 | type Game struct { 17 | scene Scene 18 | screenWidth int 19 | screenHeight int 20 | } 21 | 22 | type Config struct { 23 | Quick bool 24 | ScreenWidth int 25 | ScreenHeight int 26 | } 27 | 28 | func NewGame(config Config) *Game { 29 | assets.MustLoadAssets() 30 | 31 | g := &Game{ 32 | screenWidth: config.ScreenWidth, 33 | screenHeight: config.ScreenHeight, 34 | } 35 | 36 | if config.Quick { 37 | g.switchToGame([]system.ChosenPlayer{ 38 | { 39 | PlayerNumber: 1, 40 | Faction: 0, 41 | }, 42 | }) 43 | } else { 44 | g.switchToTitle() 45 | } 46 | 47 | return g 48 | } 49 | 50 | func (g *Game) switchToTitle() { 51 | g.scene = scene.NewTitle(g.screenWidth, g.screenHeight, g.switchToAirbase) 52 | } 53 | 54 | func (g *Game) switchToAirbase() { 55 | g.scene = scene.NewAirbase(g.screenWidth, g.screenHeight, g.switchToGame, g.switchToTitle) 56 | } 57 | 58 | func (g *Game) switchToGame(players []system.ChosenPlayer) { 59 | g.scene = scene.NewGame(players, g.screenWidth, g.screenHeight) 60 | } 61 | 62 | func (g *Game) Update() error { 63 | g.scene.Update() 64 | return nil 65 | } 66 | 67 | func (g *Game) Draw(screen *ebiten.Image) { 68 | g.scene.Draw(screen) 69 | } 70 | 71 | func (g *Game) Layout(width, height int) (int, int) { 72 | if g.screenWidth == 0 || g.screenHeight == 0 { 73 | return width, height 74 | } 75 | return g.screenWidth, g.screenHeight 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/m110/airplanes 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.0 6 | 7 | require ( 8 | github.com/hajimehoshi/ebiten/v2 v2.8.3 9 | github.com/lafriks/go-tiled v0.13.0 10 | github.com/samber/lo v1.47.0 11 | github.com/yohamta/donburi v1.15.4 12 | golang.org/x/image v0.21.0 13 | ) 14 | 15 | require ( 16 | github.com/disintegration/imaging v1.6.2 // indirect 17 | github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee // indirect 18 | github.com/ebitengine/hideconsole v1.0.0 // indirect 19 | github.com/ebitengine/purego v0.8.1 // indirect 20 | github.com/jezek/xgb v1.1.1 // indirect 21 | golang.org/x/sync v0.8.0 // indirect 22 | golang.org/x/sys v0.26.0 // indirect 23 | golang.org/x/text v0.19.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= 4 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 5 | github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee h1:YoNt0DHeZ92kjR78SfyUn1yEf7KnBypOFlFZO14cJ6w= 6 | github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee/go.mod h1:ZDIonJlTRW7gahIn5dEXZtN4cM8Qwtlduob8cOCflmg= 7 | github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= 8 | github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= 9 | github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= 10 | github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 11 | github.com/hajimehoshi/bitmapfont/v3 v3.2.0 h1:0DISQM/rseKIJhdF29AkhvdzIULqNIIlXAGWit4ez1Q= 12 | github.com/hajimehoshi/bitmapfont/v3 v3.2.0/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= 13 | github.com/hajimehoshi/ebiten/v2 v2.8.3 h1:AKHqj3QbQMzNEhK33MMJeRwXm9UzftrUUo6AWwFV258= 14 | github.com/hajimehoshi/ebiten/v2 v2.8.3/go.mod h1:SXx/whkvpfsavGo6lvZykprerakl+8Uo1X8d2U5aAnA= 15 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= 16 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 17 | github.com/lafriks/go-tiled v0.13.0 h1:xZE2rEKCNJPya+g92FCIjzEH4fZLQcZVqvpw174P2MY= 18 | github.com/lafriks/go-tiled v0.13.0/go.mod h1:FRhv/27R9S9IOmDl7+XrSUjFrV0uCUCu23rTCHRuj5c= 19 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 20 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 24 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 25 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 26 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 27 | github.com/yohamta/donburi v1.15.4 h1:R3nWrYgnhyDBn3NjtEjHMfxBdtq2m1/IiEjgdDW9vqc= 28 | github.com/yohamta/donburi v1.15.4/go.mod h1:FdjU9hpwAsAs1qRvqsSTJimPJ0dipvdnr9hMJXYc1Rk= 29 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 30 | golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= 31 | golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= 32 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 33 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 34 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 35 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 38 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | 7 | "github.com/m110/airplanes/game" 8 | 9 | "github.com/hajimehoshi/ebiten/v2" 10 | ) 11 | 12 | func main() { 13 | quickFlag := flag.Bool("quick", false, "quick mode") 14 | flag.Parse() 15 | 16 | config := game.Config{ 17 | Quick: *quickFlag, 18 | ScreenWidth: 480, 19 | ScreenHeight: 800, 20 | } 21 | 22 | ebiten.SetWindowSize(config.ScreenWidth, config.ScreenHeight) 23 | 24 | err := ebiten.RunGame(game.NewGame(config)) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes.xcodeproj/project.xcworkspace/xcuserdata/milosz.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/mobile/Airplanes/Airplanes.xcodeproj/project.xcworkspace/xcuserdata/milosz.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes.xcodeproj/xcshareddata/xcschemes/Airplanes.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes.xcodeproj/xcuserdata/milosz.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Airplanes.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 35C1A9B32CD64A7700B39F17 16 | 17 | primary 18 | 19 | 20 | 35C1A9D12CD64A7900B39F17 21 | 22 | primary 23 | 24 | 25 | 35C1A9DB2CD64A7900B39F17 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/Actions.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/mobile/Airplanes/Airplanes/Actions.sks -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | 5 | @interface AppDelegate : UIResponder 6 | 7 | @property (strong, nonatomic) UIWindow *window; 8 | 9 | 10 | @end 11 | 12 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import "AppDelegate.h" 4 | 5 | @interface AppDelegate () 6 | 7 | @end 8 | 9 | @implementation AppDelegate 10 | 11 | 12 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 13 | // Override point for customization after application launch. 14 | return YES; 15 | } 16 | 17 | 18 | - (void)applicationWillResignActive:(UIApplication *)application { 19 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 20 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 21 | } 22 | 23 | 24 | - (void)applicationDidEnterBackground:(UIApplication *)application { 25 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 26 | } 27 | 28 | 29 | - (void)applicationWillEnterForeground:(UIApplication *)application { 30 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 31 | } 32 | 33 | 34 | - (void)applicationDidBecomeActive:(UIApplication *)application { 35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 36 | } 37 | 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/GameScene.h: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | 5 | @interface GameScene : SKScene 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/GameScene.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import "GameScene.h" 4 | 5 | @implementation GameScene { 6 | SKShapeNode *_spinnyNode; 7 | SKLabelNode *_label; 8 | } 9 | 10 | - (void)didMoveToView:(SKView *)view { 11 | // Setup your scene here 12 | 13 | // Get label node from scene and store it for use later 14 | _label = (SKLabelNode *)[self childNodeWithName:@"//helloLabel"]; 15 | 16 | _label.alpha = 0.0; 17 | [_label runAction:[SKAction fadeInWithDuration:2.0]]; 18 | 19 | CGFloat w = (self.size.width + self.size.height) * 0.05; 20 | 21 | // Create shape node to use during mouse interaction 22 | _spinnyNode = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(w, w) cornerRadius:w * 0.3]; 23 | _spinnyNode.lineWidth = 2.5; 24 | 25 | [_spinnyNode runAction:[SKAction repeatActionForever:[SKAction rotateByAngle:M_PI duration:1]]]; 26 | [_spinnyNode runAction:[SKAction sequence:@[ 27 | [SKAction waitForDuration:0.5], 28 | [SKAction fadeOutWithDuration:0.5], 29 | [SKAction removeFromParent], 30 | ]]]; 31 | } 32 | 33 | 34 | - (void)touchDownAtPoint:(CGPoint)pos { 35 | SKShapeNode *n = [_spinnyNode copy]; 36 | n.position = pos; 37 | n.strokeColor = [SKColor greenColor]; 38 | [self addChild:n]; 39 | } 40 | 41 | - (void)touchMovedToPoint:(CGPoint)pos { 42 | SKShapeNode *n = [_spinnyNode copy]; 43 | n.position = pos; 44 | n.strokeColor = [SKColor blueColor]; 45 | [self addChild:n]; 46 | } 47 | 48 | - (void)touchUpAtPoint:(CGPoint)pos { 49 | SKShapeNode *n = [_spinnyNode copy]; 50 | n.position = pos; 51 | n.strokeColor = [SKColor redColor]; 52 | [self addChild:n]; 53 | } 54 | 55 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 56 | // Run 'Pulse' action from 'Actions.sks' 57 | [_label runAction:[SKAction actionNamed:@"Pulse"] withKey:@"fadeInOut"]; 58 | 59 | for (UITouch *t in touches) {[self touchDownAtPoint:[t locationInNode:self]];} 60 | } 61 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ 62 | for (UITouch *t in touches) {[self touchMovedToPoint:[t locationInNode:self]];} 63 | } 64 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 65 | for (UITouch *t in touches) {[self touchUpAtPoint:[t locationInNode:self]];} 66 | } 67 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 68 | for (UITouch *t in touches) {[self touchUpAtPoint:[t locationInNode:self]];} 69 | } 70 | 71 | 72 | -(void)update:(CFTimeInterval)currentTime { 73 | // Called before each frame is rendered 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/GameScene.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m110/airplanes/bccbc35fafc8e036cddc38f5988cc957ee4a83e0/mobile/Airplanes/Airplanes/GameScene.sks -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/GameViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | @interface GameViewController : MobileEbitenViewController 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/GameViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import "GameViewController.h" 4 | #import "GameScene.h" 5 | 6 | @implementation GameViewController 7 | 8 | - (void)viewDidLoad { 9 | [super viewDidLoad]; 10 | 11 | // Load the SKScene from 'GameScene.sks' 12 | GameScene *scene = (GameScene *)[SKScene nodeWithFileNamed:@"GameScene"]; 13 | 14 | // Set the scale mode to scale to fit the window 15 | scene.scaleMode = SKSceneScaleModeAspectFill; 16 | 17 | SKView *skView = (SKView *)self.view; 18 | 19 | // Present the scene 20 | [skView presentScene:scene]; 21 | 22 | skView.showsFPS = YES; 23 | skView.showsNodeCount = YES; 24 | } 25 | 26 | - (UIInterfaceOrientationMask)supportedInterfaceOrientations { 27 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { 28 | return UIInterfaceOrientationMaskAllButUpsideDown; 29 | } else { 30 | return UIInterfaceOrientationMaskAll; 31 | } 32 | } 33 | 34 | - (BOOL)prefersStatusBarHidden { 35 | return YES; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /mobile/Airplanes/Airplanes/main.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | #import "AppDelegate.h" 5 | 6 | int main(int argc, char * argv[]) { 7 | NSString * appDelegateClassName; 8 | @autoreleasepool { 9 | // Setup code that might create autoreleased objects goes here. 10 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 11 | } 12 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 13 | } 14 | -------------------------------------------------------------------------------- /mobile/Airplanes/AirplanesTests/AirplanesTests.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | 5 | @interface AirplanesTests : XCTestCase 6 | 7 | @end 8 | 9 | @implementation AirplanesTests 10 | 11 | - (void)setUp { 12 | // Put setup code here. This method is called before the invocation of each test method in the class. 13 | } 14 | 15 | - (void)tearDown { 16 | // Put teardown code here. This method is called after the invocation of each test method in the class. 17 | } 18 | 19 | - (void)testExample { 20 | // This is an example of a functional test case. 21 | // Use XCTAssert and related functions to verify your tests produce the correct results. 22 | } 23 | 24 | - (void)testPerformanceExample { 25 | // This is an example of a performance test case. 26 | [self measureBlock:^{ 27 | // Put the code you want to measure the time of here. 28 | }]; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /mobile/Airplanes/AirplanesUITests/AirplanesUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | 5 | @interface AirplanesUITests : XCTestCase 6 | 7 | @end 8 | 9 | @implementation AirplanesUITests 10 | 11 | - (void)setUp { 12 | // Put setup code here. This method is called before the invocation of each test method in the class. 13 | 14 | // In UI tests it is usually best to stop immediately when a failure occurs. 15 | self.continueAfterFailure = NO; 16 | 17 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 18 | } 19 | 20 | - (void)tearDown { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | - (void)testExample { 25 | // UI tests must launch the application that they test. 26 | XCUIApplication *app = [[XCUIApplication alloc] init]; 27 | [app launch]; 28 | 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | - (void)testLaunchPerformance { 33 | if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *)) { 34 | // This measures how long it takes to launch your application. 35 | [self measureWithMetrics:@[[[XCTApplicationLaunchMetric alloc] init]] block:^{ 36 | [[[XCUIApplication alloc] init] launch]; 37 | }]; 38 | } 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /mobile/Airplanes/AirplanesUITests/AirplanesUITestsLaunchTests.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import 4 | 5 | @interface AirplanesUITestsLaunchTests : XCTestCase 6 | 7 | @end 8 | 9 | @implementation AirplanesUITestsLaunchTests 10 | 11 | + (BOOL)runsForEachTargetApplicationUIConfiguration { 12 | return YES; 13 | } 14 | 15 | - (void)setUp { 16 | self.continueAfterFailure = NO; 17 | } 18 | 19 | - (void)testLaunch { 20 | XCUIApplication *app = [[XCUIApplication alloc] init]; 21 | [app launch]; 22 | 23 | // Insert steps here to perform after app launch but before taking a screenshot, 24 | // such as logging into a test account or navigating somewhere in the app 25 | 26 | XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:XCUIScreen.mainScreen.screenshot]; 27 | attachment.name = @"Launch Screen"; 28 | attachment.lifetime = XCTAttachmentLifetimeKeepAlways; 29 | [self addAttachment:attachment]; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /mobile/main.go: -------------------------------------------------------------------------------- 1 | package mobile 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2/mobile" 5 | 6 | "github.com/m110/airplanes/game" 7 | ) 8 | 9 | func init() { 10 | mobile.SetGame(game.NewGame(game.Config{ 11 | Quick: true, 12 | ScreenWidth: 480, 13 | ScreenHeight: 1040, 14 | })) 15 | } 16 | 17 | func Dummy() {} 18 | -------------------------------------------------------------------------------- /scene/airbase.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/yohamta/donburi" 6 | "github.com/yohamta/donburi/features/math" 7 | "github.com/yohamta/donburi/features/transform" 8 | 9 | "github.com/m110/airplanes/archetype" 10 | "github.com/m110/airplanes/assets" 11 | "github.com/m110/airplanes/component" 12 | "github.com/m110/airplanes/system" 13 | ) 14 | 15 | type Airbase struct { 16 | world donburi.World 17 | systems []System 18 | drawables []Drawable 19 | 20 | width int 21 | height int 22 | 23 | startCallback system.StartGameCallback 24 | backToMenuCallback func() 25 | } 26 | 27 | func NewAirbase(width, height int, startCallback system.StartGameCallback, backToMenuCallback func()) *Airbase { 28 | a := &Airbase{ 29 | startCallback: startCallback, 30 | backToMenuCallback: backToMenuCallback, 31 | width: width, 32 | height: height, 33 | } 34 | 35 | a.createWorld() 36 | 37 | return a 38 | } 39 | 40 | func (a *Airbase) createWorld() { 41 | render := system.NewRenderer() 42 | debug := system.NewDebug(a.createWorld) 43 | 44 | a.systems = []System{ 45 | system.NewVelocity(), 46 | system.NewPlayerSelect(a.startCallback, a.backToMenuCallback), 47 | system.NewAltitude(), 48 | debug, 49 | render, 50 | } 51 | 52 | a.drawables = []Drawable{ 53 | render, 54 | system.NewLabel(), 55 | debug, 56 | } 57 | 58 | levelAsset := assets.AirBase 59 | a.world = donburi.NewWorld() 60 | 61 | levelEntry := a.world.Entry( 62 | a.world.Create(transform.Transform, component.Sprite), 63 | ) 64 | 65 | component.Sprite.SetValue(levelEntry, component.SpriteData{ 66 | Image: levelAsset.Background, 67 | Layer: component.SpriteLayerBackground, 68 | Pivot: component.SpritePivotTopLeft, 69 | }) 70 | 71 | archetype.NewCamera(a.world, math.Vec2{}) 72 | 73 | for i, spawn := range levelAsset.Spawns { 74 | archetype.NewAirbaseAirplane(a.world, spawn.Position, component.MustPlayerFactionFromString(spawn.Faction), i) 75 | } 76 | 77 | a.world.Create(component.Debug) 78 | } 79 | 80 | func (a *Airbase) Update() { 81 | for _, s := range a.systems { 82 | s.Update(a.world) 83 | } 84 | } 85 | 86 | func (a *Airbase) Draw(screen *ebiten.Image) { 87 | screen.Clear() 88 | for _, s := range a.drawables { 89 | s.Draw(a.world, screen) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /scene/game.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/inpututil" 8 | "github.com/yohamta/donburi" 9 | "github.com/yohamta/donburi/features/math" 10 | "github.com/yohamta/donburi/features/transform" 11 | "github.com/yohamta/donburi/filter" 12 | 13 | "github.com/m110/airplanes/archetype" 14 | "github.com/m110/airplanes/assets" 15 | "github.com/m110/airplanes/component" 16 | "github.com/m110/airplanes/engine" 17 | "github.com/m110/airplanes/system" 18 | ) 19 | 20 | type System interface { 21 | Update(w donburi.World) 22 | } 23 | 24 | type Drawable interface { 25 | Draw(w donburi.World, screen *ebiten.Image) 26 | } 27 | 28 | type Game struct { 29 | players []system.ChosenPlayer 30 | level int 31 | world donburi.World 32 | systems []System 33 | drawables []Drawable 34 | 35 | screenWidth int 36 | screenHeight int 37 | } 38 | 39 | func NewGame(players []system.ChosenPlayer, screenWidth int, screenHeight int) *Game { 40 | g := &Game{ 41 | players: players, 42 | level: 0, 43 | screenWidth: screenWidth, 44 | screenHeight: screenHeight, 45 | } 46 | 47 | g.loadLevel() 48 | 49 | return g 50 | } 51 | 52 | func (g *Game) nextLevel() { 53 | if g.level == len(assets.Levels)-1 { 54 | // TODO all levels done 55 | return 56 | } 57 | g.level++ 58 | g.loadLevel() 59 | } 60 | 61 | func (g *Game) loadLevel() { 62 | render := system.NewRenderer() 63 | debug := system.NewDebug(g.loadLevel) 64 | 65 | g.systems = []System{ 66 | system.NewControls(), 67 | system.NewVelocity(), 68 | system.NewBounds(), 69 | system.NewCameraBounds(), 70 | system.NewSpawn(), 71 | system.NewAI(), 72 | system.NewDespawn(), 73 | system.NewCollision(), 74 | system.NewProgression(g.nextLevel), 75 | system.NewHealth(), 76 | system.NewRespawn(g.restart), 77 | system.NewInvulnerable(), 78 | system.NewCamera(), 79 | system.NewObserver(), 80 | system.NewShooter(), 81 | system.NewEvolution(), 82 | system.NewAltitude(), 83 | system.NewEvents(), 84 | system.NewFollower(), 85 | system.NewDistanceLimit(), 86 | render, 87 | debug, 88 | system.NewTimeToLive(), 89 | system.NewDestroy(), 90 | } 91 | 92 | g.drawables = []Drawable{ 93 | render, 94 | debug, 95 | system.NewHUD(), 96 | } 97 | 98 | g.world = g.createWorld(g.level) 99 | } 100 | 101 | func (g *Game) createWorld(levelIndex int) donburi.World { 102 | levelAsset := assets.Levels[levelIndex] 103 | 104 | world := donburi.NewWorld() 105 | 106 | level := world.Entry(world.Create(component.Level)) 107 | component.Level.Get(level).ProgressionTimer = engine.NewTimer(time.Second * 3) 108 | 109 | archetype.NewCamera(world, math.Vec2{ 110 | X: 0, 111 | Y: float64(levelAsset.Background.Bounds().Dy() - g.screenHeight), 112 | }) 113 | 114 | levelEntry := world.Entry( 115 | world.Create(transform.Transform, component.Sprite), 116 | ) 117 | component.Sprite.SetValue(levelEntry, component.SpriteData{ 118 | Image: levelAsset.Background, 119 | Layer: component.SpriteLayerBackground, 120 | Pivot: component.SpritePivotTopLeft, 121 | }) 122 | 123 | for i := range levelAsset.Enemies { 124 | enemy := levelAsset.Enemies[i] 125 | pos := enemy.Position 126 | // TODO Sprite offset could be based on the sprite 127 | pos.Y += 32 128 | 129 | archetype.NewEnemySpawn(world, pos, func(w donburi.World) { 130 | enemyToSpawnFunc(enemy)(w) 131 | }) 132 | } 133 | 134 | for i := range levelAsset.EnemyGroupSpawns { 135 | groupSpawn := levelAsset.EnemyGroupSpawns[i] 136 | pos := groupSpawn.Position 137 | archetype.NewEnemySpawn(world, pos, func(w donburi.World) { 138 | for _, enemy := range groupSpawn.Enemies { 139 | spawnFunc := enemyToSpawnFunc(enemy) 140 | spawnFunc(w) 141 | } 142 | }) 143 | } 144 | 145 | if g.world == nil { 146 | game := world.Entry(world.Create(component.Game)) 147 | component.Game.SetValue(game, component.GameData{ 148 | Score: 0, 149 | Settings: component.Settings{ 150 | ScreenWidth: g.screenWidth, 151 | ScreenHeight: g.screenHeight, 152 | }, 153 | }) 154 | 155 | // Spawn new players 156 | for _, p := range g.players { 157 | player := archetype.NewPlayer(world, p.PlayerNumber, p.Faction) 158 | archetype.NewPlayerAirplane(world, *component.Player.Get(player), p.Faction, 0) 159 | } 160 | } else { 161 | // Keep the same game data across levels 162 | gameData := component.MustFindGame(g.world) 163 | newGameData := world.Entry(world.Create(component.Game)) 164 | component.Game.Set(newGameData, gameData) 165 | 166 | // Transfer existing players from the previous level 167 | donburi.NewQuery(filter.Contains(component.Player)).Each(g.world, func(entry *donburi.Entry) { 168 | player := component.Player.Get(entry) 169 | // In case the level ends while the player's respawning 170 | player.Respawning = false 171 | 172 | archetype.NewPlayerFromPlayerData(world, *player) 173 | if player.Lives > 0 { 174 | archetype.NewPlayerAirplane(world, *player, player.PlayerFaction, player.EvolutionLevel()) 175 | } 176 | }) 177 | } 178 | 179 | world.Create(component.Debug) 180 | 181 | system.SetupEvents(world) 182 | 183 | return world 184 | } 185 | 186 | func enemyToSpawnFunc(enemy assets.Enemy) func(w donburi.World) { 187 | switch enemy.Class { 188 | case assets.EnemyClassAirplane: 189 | return func(w donburi.World) { 190 | archetype.NewEnemyAirplane( 191 | w, 192 | enemy.Position, 193 | enemy.Rotation, 194 | enemy.Speed, 195 | enemy.Path, 196 | ) 197 | } 198 | case assets.EnemyClassTank: 199 | return func(w donburi.World) { 200 | archetype.NewEnemyTank( 201 | w, 202 | enemy.Position, 203 | enemy.Rotation, 204 | enemy.Speed, 205 | enemy.Path, 206 | ) 207 | } 208 | case assets.EnemyClassTurretBeam: 209 | return func(w donburi.World) { 210 | archetype.NewEnemyTurretBeam( 211 | w, 212 | enemy.Position, 213 | enemy.Rotation, 214 | ) 215 | } 216 | case assets.EnemyClassTurretMissiles: 217 | return func(w donburi.World) { 218 | archetype.NewEnemyTurretMissiles( 219 | w, 220 | enemy.Position, 221 | enemy.Rotation, 222 | ) 223 | } 224 | default: 225 | panic("unknown enemy class: " + enemy.Class) 226 | } 227 | } 228 | 229 | func (g *Game) restart() { 230 | g.world = nil 231 | g.level = 0 232 | g.loadLevel() 233 | } 234 | 235 | func (g *Game) Update() { 236 | gameData := component.MustFindGame(g.world) 237 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 238 | gameData.Paused = !gameData.Paused 239 | } 240 | 241 | if gameData.Paused { 242 | return 243 | } 244 | 245 | for _, s := range g.systems { 246 | s.Update(g.world) 247 | } 248 | } 249 | 250 | func (g *Game) Draw(screen *ebiten.Image) { 251 | screen.Clear() 252 | for _, s := range g.drawables { 253 | s.Draw(g.world, screen) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /scene/title.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2/inpututil" 5 | "image/color" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/hajimehoshi/ebiten/v2/text" 9 | 10 | "github.com/m110/airplanes/assets" 11 | ) 12 | 13 | type Title struct { 14 | screenWidth int 15 | screenHeight int 16 | newGameCallback func() 17 | } 18 | 19 | func NewTitle(screenWidth int, screenHeight int, newGameCallback func()) *Title { 20 | return &Title{ 21 | screenWidth: screenWidth, 22 | screenHeight: screenHeight, 23 | newGameCallback: newGameCallback, 24 | } 25 | } 26 | 27 | func (t *Title) Update() { 28 | if ebiten.IsKeyPressed(ebiten.KeyEnter) || ebiten.IsKeyPressed(ebiten.KeySpace) { 29 | t.newGameCallback() 30 | return 31 | } 32 | 33 | touchIDs := inpututil.AppendJustPressedTouchIDs(nil) 34 | if len(touchIDs) > 0 { 35 | t.newGameCallback() 36 | return 37 | } 38 | } 39 | 40 | func (t *Title) Draw(screen *ebiten.Image) { 41 | text.Draw(screen, "m110's Airplanes", assets.NarrowFont, t.screenWidth/4, 100, color.White) 42 | text.Draw(screen, "Player 1: WASD + Space", assets.NarrowFont, t.screenWidth/6, 250, color.White) 43 | text.Draw(screen, "Player 2: Arrows + Enter", assets.NarrowFont, t.screenWidth/6, 350, color.White) 44 | text.Draw(screen, "Press space to start", assets.NarrowFont, t.screenWidth/5, 500, color.White) 45 | } 46 | -------------------------------------------------------------------------------- /system/ai.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/yohamta/donburi" 7 | dmath "github.com/yohamta/donburi/features/math" 8 | "github.com/yohamta/donburi/features/transform" 9 | "github.com/yohamta/donburi/filter" 10 | 11 | "github.com/m110/airplanes/component" 12 | ) 13 | 14 | type AI struct { 15 | query *donburi.Query 16 | } 17 | 18 | func NewAI() *AI { 19 | return &AI{ 20 | query: donburi.NewQuery( 21 | filter.Contains( 22 | transform.Transform, 23 | component.Velocity, 24 | component.AI, 25 | ), 26 | ), 27 | } 28 | } 29 | 30 | func (a *AI) Update(w donburi.World) { 31 | a.query.Each(w, func(entry *donburi.Entry) { 32 | ai := component.AI.Get(entry) 33 | if ai.Type == component.AITypeFollowPath { 34 | if ai.NextTarget >= len(ai.Path) { 35 | return 36 | } 37 | 38 | position := transform.Transform.Get(entry).LocalPosition 39 | velocity := component.Velocity.Get(entry) 40 | 41 | target := ai.Path[ai.NextTarget] 42 | 43 | x := target.X - position.X 44 | y := target.Y - position.Y 45 | 46 | dist := math.Sqrt(x*x + y*y) 47 | if dist < 1 { 48 | ai.NextTarget++ 49 | if ai.PathLoops && ai.NextTarget >= len(ai.Path) { 50 | ai.NextTarget = 0 51 | } 52 | return 53 | } 54 | 55 | worldRotation := transform.WorldRotation(entry) 56 | 57 | // TODO Could be simplified perhaps ^^' 58 | angle := math.Round(dmath.ToDegrees(math.Atan2(y, x))) 59 | 60 | // TODO Learn trigonometry 61 | if worldRotation-angle > 180.0 { 62 | angle = float64(int(angle+360.0) % 360) 63 | } else if worldRotation-angle < -180.0 { 64 | angle = float64(int(angle-360.0) % 360) 65 | } 66 | 67 | maxRotation := 2.0 * ai.Speed 68 | targetAngle := angle 69 | 70 | diff := targetAngle - worldRotation 71 | if math.Abs(diff) > maxRotation { 72 | if diff > 0 { 73 | diff = maxRotation 74 | } else { 75 | diff = -maxRotation 76 | } 77 | } 78 | 79 | transform.Transform.Get(entry).LocalRotation += diff 80 | 81 | // TODO Should use transform.Right() instead but it doesn't work 82 | radians := dmath.ToRadians(angle) 83 | velocity.Velocity.X = math.Cos(radians) * ai.Speed 84 | velocity.Velocity.Y = math.Sin(radians) * ai.Speed 85 | } else if ai.Type == component.AITypeConstantVelocity && !ai.StartedMoving { 86 | velocity := component.Velocity.Get(entry) 87 | velocity.Velocity = transform.Right(entry).MulScalar(ai.Speed) 88 | ai.StartedMoving = true 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /system/altitude.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | "github.com/yohamta/donburi/features/transform" 7 | "github.com/yohamta/donburi/filter" 8 | 9 | "github.com/m110/airplanes/archetype" 10 | "github.com/m110/airplanes/component" 11 | ) 12 | 13 | type Altitude struct { 14 | query *donburi.Query 15 | } 16 | 17 | func NewAltitude() *Altitude { 18 | return &Altitude{ 19 | query: donburi.NewQuery(filter.Contains( 20 | transform.Transform, 21 | component.Altitude, 22 | )), 23 | } 24 | } 25 | 26 | func (a *Altitude) Update(w donburi.World) { 27 | a.query.Each(w, func(entry *donburi.Entry) { 28 | altitude := component.Altitude.Get(entry) 29 | altitude.Update() 30 | 31 | scale := 0.8 + 0.2*altitude.Altitude 32 | t := transform.Transform.Get(entry) 33 | t.LocalScale.X = scale 34 | t.LocalScale.Y = scale 35 | 36 | shadow, ok := transform.FindChildWithComponent(entry, component.ShadowTag) 37 | if ok { 38 | shadowTransform := transform.Transform.Get(shadow) 39 | shadowTransform.LocalPosition.X = -archetype.MaxShadowPosition * altitude.Altitude 40 | shadowTransform.LocalPosition.Y = archetype.MaxShadowPosition * altitude.Altitude 41 | } 42 | 43 | // Grounded units don't move 44 | if altitude.Falling && altitude.Altitude == 0 { 45 | if entry.HasComponent(component.Velocity) { 46 | velocity := component.Velocity.Get(entry) 47 | velocity.Velocity = math.Vec2{} 48 | velocity.RotationVelocity = 0 49 | } 50 | sprite := component.Sprite.Get(entry) 51 | sprite.Layer = component.SpriteLayerDebris 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /system/bounds.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/archetype" 9 | "github.com/m110/airplanes/component" 10 | "github.com/m110/airplanes/engine" 11 | ) 12 | 13 | type Bounds struct { 14 | query *donburi.Query 15 | game *component.GameData 16 | } 17 | 18 | func NewBounds() *Bounds { 19 | return &Bounds{ 20 | query: donburi.NewQuery(filter.Contains( 21 | component.PlayerAirplane, 22 | transform.Transform, 23 | component.Sprite, 24 | component.Bounds, 25 | )), 26 | } 27 | } 28 | 29 | func (b *Bounds) Update(w donburi.World) { 30 | if b.game == nil { 31 | b.game = component.MustFindGame(w) 32 | if b.game == nil { 33 | return 34 | } 35 | } 36 | 37 | camera := archetype.MustFindCamera(w) 38 | cameraPos := transform.Transform.Get(camera).LocalPosition 39 | 40 | b.query.Each(w, func(entry *donburi.Entry) { 41 | bounds := component.Bounds.Get(entry) 42 | if bounds.Disabled { 43 | return 44 | } 45 | 46 | t := transform.Transform.Get(entry) 47 | sprite := component.Sprite.Get(entry) 48 | 49 | w, h := sprite.Image.Size() 50 | width, height := float64(w), float64(h) 51 | 52 | var minX, maxX, minY, maxY float64 53 | 54 | switch sprite.Pivot { 55 | case component.SpritePivotTopLeft: 56 | minX = cameraPos.X 57 | maxX = cameraPos.X + float64(b.game.Settings.ScreenWidth) - width 58 | 59 | minY = cameraPos.Y 60 | maxY = cameraPos.Y + float64(b.game.Settings.ScreenHeight) - height 61 | case component.SpritePivotCenter: 62 | minX = cameraPos.X + width/2 63 | maxX = cameraPos.X + float64(b.game.Settings.ScreenWidth) - width/2 64 | 65 | minY = cameraPos.Y + height/2 66 | maxY = cameraPos.Y + float64(b.game.Settings.ScreenHeight) - height/2 67 | } 68 | 69 | t.LocalPosition.X = engine.Clamp(t.LocalPosition.X, minX, maxX) 70 | t.LocalPosition.Y = engine.Clamp(t.LocalPosition.Y, minY, maxY) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /system/camera.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | 6 | "github.com/m110/airplanes/archetype" 7 | "github.com/m110/airplanes/component" 8 | ) 9 | 10 | type Camera struct{} 11 | 12 | func NewCamera() *Camera { 13 | return &Camera{} 14 | } 15 | 16 | func (c *Camera) Update(w donburi.World) { 17 | camera := archetype.MustFindCamera(w) 18 | cam := component.Camera.Get(camera) 19 | 20 | if !cam.Moving { 21 | cam.MoveTimer.Update() 22 | if cam.MoveTimer.IsReady() { 23 | cam.Moving = true 24 | component.Velocity.Get(camera).Velocity.Y = -0.5 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /system/camerabounds.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type CameraBounds struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewCameraBounds() *CameraBounds { 16 | return &CameraBounds{ 17 | query: donburi.NewQuery(filter.Contains( 18 | component.Camera, 19 | transform.Transform, 20 | )), 21 | } 22 | } 23 | 24 | func (b *CameraBounds) Update(w donburi.World) { 25 | b.query.Each(w, func(entry *donburi.Entry) { 26 | t := transform.Transform.Get(entry) 27 | if t.LocalPosition.X < 0 { 28 | t.LocalPosition.X = 0 29 | } 30 | 31 | if t.LocalPosition.Y < 0 { 32 | t.LocalPosition.Y = 0 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /system/collision.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yohamta/donburi" 7 | "github.com/yohamta/donburi/features/transform" 8 | "github.com/yohamta/donburi/filter" 9 | 10 | "github.com/m110/airplanes/archetype" 11 | "github.com/m110/airplanes/component" 12 | "github.com/m110/airplanes/engine" 13 | ) 14 | 15 | type Collision struct { 16 | query *donburi.Query 17 | } 18 | 19 | func NewCollision() *Collision { 20 | return &Collision{ 21 | query: donburi.NewQuery(filter.Contains(component.Collider)), 22 | } 23 | } 24 | 25 | type collisionEffect func(w donburi.World, entry *donburi.Entry, other *donburi.Entry) 26 | 27 | var collisionEffects = map[component.ColliderLayer]map[component.ColliderLayer]collisionEffect{ 28 | component.CollisionLayerPlayerBullets: { 29 | component.CollisionLayerAirEnemies: func(w donburi.World, entry *donburi.Entry, other *donburi.Entry) { 30 | component.Destroy(entry) 31 | component.Health.Get(other).Damage() 32 | }, 33 | component.CollisionLayerGroundEnemies: func(w donburi.World, entry *donburi.Entry, other *donburi.Entry) { 34 | component.Destroy(entry) 35 | component.Health.Get(other).Damage() 36 | }, 37 | }, 38 | component.CollisionLayerPlayers: { 39 | component.CollisionLayerCollectibles: func(w donburi.World, entry *donburi.Entry, other *donburi.Entry) { 40 | airplane := component.PlayerAirplane.Get(entry) 41 | player := archetype.MustFindPlayerByNumber(w, airplane.PlayerNumber) 42 | 43 | // TODO Is this the best place to do this? 44 | switch component.Collectible.Get(other).Type { 45 | case component.CollectibleTypeWeaponUpgrade: 46 | player.UpgradeWeapon() 47 | 48 | evolution := component.Evolution.Get(entry) 49 | if player.EvolutionLevel() > evolution.Level { 50 | evolution.Evolve() 51 | } 52 | case component.CollectibleTypeShield: 53 | airplane.StartInvulnerability() 54 | case component.CollectibleTypeHealth: 55 | player.AddLive() 56 | } 57 | 58 | component.Destroy(other) 59 | }, 60 | }, 61 | component.CollisionLayerAirEnemies: { 62 | component.CollisionLayerPlayers: func(w donburi.World, entry *donburi.Entry, other *donburi.Entry) { 63 | EnemyKilledEvent.Publish(w, EnemyKilled{ 64 | Enemy: entry, 65 | }) 66 | 67 | damagePlayer(w, other) 68 | }, 69 | }, 70 | component.CollisionLayerEnemyBullets: { 71 | component.CollisionLayerPlayers: func(w donburi.World, entry *donburi.Entry, other *donburi.Entry) { 72 | component.Destroy(entry) 73 | damagePlayer(w, other) 74 | }, 75 | }, 76 | } 77 | 78 | func damagePlayer(w donburi.World, entry *donburi.Entry) { 79 | if component.PlayerAirplane.Get(entry).Invulnerable { 80 | return 81 | } 82 | 83 | playerNumber := component.PlayerAirplane.Get(entry).PlayerNumber 84 | 85 | if entry.HasComponent(component.Wreckable) { 86 | archetype.NewAirplaneWreck(w, entry, component.Sprite.Get(entry)) 87 | } 88 | component.Destroy(entry) 89 | 90 | player := archetype.MustFindPlayerByNumber(w, playerNumber) 91 | player.Damage() 92 | } 93 | 94 | func (c *Collision) Update(w donburi.World) { 95 | var entries []*donburi.Entry 96 | c.query.Each(w, func(entry *donburi.Entry) { 97 | // Skip entities not spawned yet 98 | if entry.HasComponent(component.Despawnable) { 99 | if !component.Despawnable.Get(entry).Spawned { 100 | return 101 | } 102 | } 103 | entries = append(entries, entry) 104 | }) 105 | 106 | for _, entry := range entries { 107 | if !entry.Valid() { 108 | continue 109 | } 110 | 111 | collider := component.Collider.Get(entry) 112 | 113 | for _, other := range entries { 114 | if entry.Entity().Id() == other.Entity().Id() { 115 | continue 116 | } 117 | 118 | otherCollider := component.Collider.Get(other) 119 | 120 | effects, ok := collisionEffects[collider.Layer] 121 | if !ok { 122 | continue 123 | } 124 | 125 | effect, ok := effects[otherCollider.Layer] 126 | if !ok { 127 | continue 128 | } 129 | 130 | if !entry.HasComponent(transform.Transform) { 131 | panic(fmt.Sprintf("%#v missing position\n", entry.Entity().Id())) 132 | } 133 | pos := transform.Transform.Get(entry).LocalPosition 134 | otherPos := transform.Transform.Get(other).LocalPosition 135 | 136 | // TODO The current approach doesn't take rotation into account 137 | // TODO The current approach doesn't take scale into account 138 | rect := engine.NewRect(pos.X, pos.Y, collider.Width, collider.Height) 139 | otherRect := engine.NewRect(otherPos.X, otherPos.Y, otherCollider.Width, otherCollider.Height) 140 | 141 | if rect.Intersects(otherRect) { 142 | effect(w, entry, other) 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /system/controls.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | stdmath "math" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/yohamta/donburi" 9 | "github.com/yohamta/donburi/features/hierarchy" 10 | "github.com/yohamta/donburi/features/math" 11 | "github.com/yohamta/donburi/features/transform" 12 | "github.com/yohamta/donburi/filter" 13 | 14 | "github.com/m110/airplanes/archetype" 15 | "github.com/m110/airplanes/component" 16 | "github.com/m110/airplanes/engine" 17 | "github.com/m110/airplanes/engine/input" 18 | ) 19 | 20 | type Controls struct { 21 | query *donburi.Query 22 | } 23 | 24 | func NewControls() *Controls { 25 | return &Controls{ 26 | query: donburi.NewQuery( 27 | filter.Contains( 28 | transform.Transform, 29 | component.Input, 30 | component.Velocity, 31 | component.Sprite, 32 | component.PlayerAirplane, 33 | ), 34 | ), 35 | } 36 | } 37 | 38 | func (i *Controls) Update(w donburi.World) { 39 | i.query.Each(w, func(entry *donburi.Entry) { 40 | in := component.Input.Get(entry) 41 | 42 | var isTouch bool 43 | var touchX, touchY int 44 | if input.IsTouchPrimaryInput() { 45 | touchIDs := ebiten.AppendTouchIDs(nil) 46 | if len(touchIDs) > 0 { 47 | isTouch = true 48 | touchX, touchY = ebiten.TouchPosition(touchIDs[0]) 49 | } 50 | } else { 51 | if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { 52 | isTouch = true 53 | touchX, touchY = ebiten.CursorPosition() 54 | } 55 | } 56 | 57 | var joystickMovement *math.Vec2 58 | joystick, joystickFound := engine.FindWithComponent(w, component.Joystick) 59 | if isTouch { 60 | if !joystickFound { 61 | fmt.Println(touchX, touchY) 62 | joystick = archetype.NewJoystick(w, math.Vec2{ 63 | X: float64(touchX), 64 | Y: float64(touchY), 65 | }) 66 | } 67 | joystickPos := transform.WorldPosition(joystick) 68 | knobMove := math.Vec2{ 69 | X: float64(touchX) - joystickPos.X, 70 | Y: float64(touchY) - joystickPos.Y, 71 | } 72 | maxRadius := 20.0 73 | length := knobMove.Magnitude() 74 | 75 | // Get direction vector before clamping (this will be normalized) 76 | direction := knobMove 77 | if length > 0 { 78 | direction = direction.DivScalar(length) // Normalize to length 1 79 | } 80 | 81 | // Clamp the knob movement 82 | if length > maxRadius { 83 | knobMove.X = knobMove.X / length * maxRadius 84 | knobMove.Y = knobMove.Y / length * maxRadius 85 | } 86 | 87 | movementFactor := stdmath.Min(length/maxRadius, 1.0) 88 | 89 | knob := hierarchy.MustGetChildren(joystick)[0] 90 | knobTransform := transform.Transform.Get(knob) 91 | knobTransform.LocalPosition = knobMove 92 | 93 | // Calculate velocity using the normalized direction vector 94 | playerSpeed := in.MoveSpeed * movementFactor 95 | move := direction.MulScalar(playerSpeed) 96 | joystickMovement = &move 97 | } else { 98 | if joystickFound { 99 | component.Destroy(joystick) 100 | } 101 | } 102 | 103 | if in.Disabled { 104 | return 105 | } 106 | 107 | velocity := component.Velocity.Get(entry) 108 | 109 | velocity.Velocity = math.Vec2{ 110 | X: 0, 111 | // TODO should match camera scroll speed, get this from settings? 112 | Y: -0.5, 113 | } 114 | 115 | if joystickMovement != nil { 116 | velocity.Velocity = *joystickMovement 117 | } 118 | 119 | if ebiten.IsKeyPressed(in.MoveUpKey) { 120 | velocity.Velocity.Y = -in.MoveSpeed 121 | } else if ebiten.IsKeyPressed(in.MoveDownKey) { 122 | velocity.Velocity.Y = in.MoveSpeed 123 | } 124 | 125 | if ebiten.IsKeyPressed(in.MoveRightKey) { 126 | velocity.Velocity.X = in.MoveSpeed 127 | } 128 | if ebiten.IsKeyPressed(in.MoveLeftKey) { 129 | velocity.Velocity.X = -in.MoveSpeed 130 | } 131 | 132 | // TODO Seems like a very complex way to get the weapon level and timer 133 | airplane := component.PlayerAirplane.Get(entry) 134 | player := archetype.MustFindPlayerByNumber(w, airplane.PlayerNumber) 135 | player.ShootTimer.Update() 136 | if (ebiten.IsKeyPressed(in.ShootKey) || input.IsTouchPrimaryInput()) && player.ShootTimer.IsReady() { 137 | position := transform.Transform.Get(entry).LocalPosition 138 | 139 | archetype.NewPlayerBullet(w, player, position) 140 | player.ShootTimer.Reset() 141 | } 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /system/debug.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 8 | "github.com/hajimehoshi/ebiten/v2/inpututil" 9 | "github.com/yohamta/donburi" 10 | "github.com/yohamta/donburi/features/math" 11 | "github.com/yohamta/donburi/features/transform" 12 | "github.com/yohamta/donburi/filter" 13 | "golang.org/x/image/colornames" 14 | 15 | "github.com/m110/airplanes/archetype" 16 | "github.com/m110/airplanes/component" 17 | ) 18 | 19 | type Debug struct { 20 | query *donburi.Query 21 | debug *component.DebugData 22 | offscreen *ebiten.Image 23 | 24 | pausedCameraVelocity math.Vec2 25 | 26 | restartLevelCallback func() 27 | } 28 | 29 | func NewDebug(restartLevelCallback func()) *Debug { 30 | return &Debug{ 31 | query: donburi.NewQuery( 32 | filter.Contains(transform.Transform, component.Sprite), 33 | ), 34 | // TODO figure out the proper size 35 | offscreen: ebiten.NewImage(3000, 3000), 36 | restartLevelCallback: restartLevelCallback, 37 | } 38 | } 39 | 40 | func (d *Debug) Update(w donburi.World) { 41 | if d.debug == nil { 42 | debug, ok := donburi.NewQuery(filter.Contains(component.Debug)).First(w) 43 | if !ok { 44 | return 45 | } 46 | 47 | d.debug = component.Debug.Get(debug) 48 | } 49 | 50 | if inpututil.IsKeyJustPressed(ebiten.KeySlash) { 51 | d.debug.Enabled = !d.debug.Enabled 52 | } 53 | 54 | if d.debug.Enabled { 55 | if inpututil.IsKeyJustPressed(ebiten.Key1) { 56 | donburi.NewQuery(filter.Contains(component.Player)).Each(w, func(entry *donburi.Entry) { 57 | player := component.Player.Get(entry) 58 | player.UpgradeWeapon() 59 | }) 60 | } 61 | if inpututil.IsKeyJustPressed(ebiten.KeyQ) { 62 | donburi.NewQuery(filter.Contains(component.PlayerAirplane)).Each(w, func(entry *donburi.Entry) { 63 | t := transform.Transform.Get(entry) 64 | t.LocalRotation -= 10 65 | }) 66 | } 67 | if inpututil.IsKeyJustPressed(ebiten.KeyE) { 68 | donburi.NewQuery(filter.Contains(component.PlayerAirplane)).Each(w, func(entry *donburi.Entry) { 69 | t := transform.Transform.Get(entry) 70 | t.LocalRotation += 10 71 | }) 72 | } 73 | if inpututil.IsKeyJustPressed(ebiten.KeyV) { 74 | donburi.NewQuery(filter.Contains(component.PlayerAirplane)).Each(w, func(entry *donburi.Entry) { 75 | component.Evolution.Get(entry).Evolve() 76 | }) 77 | } 78 | if inpututil.IsKeyJustPressed(ebiten.KeyP) { 79 | velocity := component.Velocity.Get(archetype.MustFindCamera(w)) 80 | if d.pausedCameraVelocity.IsZero() { 81 | d.pausedCameraVelocity = velocity.Velocity 82 | velocity.Velocity = math.Vec2{} 83 | } else { 84 | velocity.Velocity = d.pausedCameraVelocity 85 | d.pausedCameraVelocity = math.Vec2{} 86 | } 87 | } 88 | if inpututil.IsKeyJustPressed(ebiten.KeyR) { 89 | d.restartLevelCallback() 90 | } 91 | } 92 | } 93 | 94 | func (d *Debug) Draw(w donburi.World, screen *ebiten.Image) { 95 | if d.debug == nil || !d.debug.Enabled { 96 | return 97 | } 98 | 99 | allCount := w.Len() 100 | 101 | despawnableCount := 0 102 | spawnedCount := 0 103 | donburi.NewQuery(filter.Contains(component.Despawnable)).Each(w, func(entry *donburi.Entry) { 104 | despawnableCount++ 105 | if component.Despawnable.Get(entry).Spawned { 106 | spawnedCount++ 107 | } 108 | }) 109 | 110 | ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Entities: %v Despawnable: %v Spawned: %v", allCount, despawnableCount, spawnedCount), 0, 0) 111 | ebitenutil.DebugPrintAt(screen, fmt.Sprintf("TPS: %v", ebiten.ActualTPS()), 0, 20) 112 | 113 | d.offscreen.Clear() 114 | d.query.Each(w, func(entry *donburi.Entry) { 115 | t := transform.Transform.Get(entry) 116 | sprite := component.Sprite.Get(entry) 117 | 118 | position := transform.WorldPosition(entry) 119 | 120 | w, h := sprite.Image.Size() 121 | halfW, halfH := float64(w)/2, float64(h)/2 122 | 123 | x := position.X 124 | y := position.Y 125 | 126 | switch sprite.Pivot { 127 | case component.SpritePivotCenter: 128 | x -= halfW 129 | y -= halfH 130 | } 131 | 132 | ebitenutil.DrawRect(d.offscreen, t.LocalPosition.X-2, t.LocalPosition.Y-2, 4, 4, colornames.Lime) 133 | ebitenutil.DebugPrintAt(d.offscreen, fmt.Sprintf("%v", entry.Entity().Id()), int(x), int(y)) 134 | ebitenutil.DebugPrintAt(d.offscreen, fmt.Sprintf("pos: %.0f, %.0f", position.X, position.Y), int(x), int(y)+40) 135 | ebitenutil.DebugPrintAt(d.offscreen, fmt.Sprintf("rot: %.0f", transform.WorldRotation(entry)), int(x), int(y)+60) 136 | 137 | length := 50.0 138 | right := position.Add(transform.Right(entry).MulScalar(length)) 139 | up := position.Add(transform.Up(entry).MulScalar(length)) 140 | 141 | ebitenutil.DrawLine(d.offscreen, position.X, position.Y, right.X, right.Y, colornames.Blue) 142 | ebitenutil.DrawLine(d.offscreen, position.X, position.Y, up.X, up.Y, colornames.Lime) 143 | 144 | if entry.HasComponent(component.Collider) { 145 | collider := component.Collider.Get(entry) 146 | ebitenutil.DrawLine(d.offscreen, x, y, x+collider.Width, y, colornames.Lime) 147 | ebitenutil.DrawLine(d.offscreen, x, y, x, y+collider.Height, colornames.Lime) 148 | ebitenutil.DrawLine(d.offscreen, x+collider.Width, y, x+collider.Width, y+collider.Height, colornames.Lime) 149 | ebitenutil.DrawLine(d.offscreen, x, y+collider.Height, x+collider.Width, y+collider.Height, colornames.Lime) 150 | } 151 | 152 | if entry.HasComponent(component.AI) { 153 | ai := component.AI.Get(entry) 154 | for i, p := range ai.Path { 155 | ebitenutil.DrawRect(d.offscreen, p.X-2, p.Y-2, 4, 4, colornames.Red) 156 | if i < len(ai.Path)-1 { 157 | next := ai.Path[i+1] 158 | ebitenutil.DrawLine(d.offscreen, p.X, p.Y, next.X, next.Y, colornames.Red) 159 | } else if ai.PathLoops { 160 | next := ai.Path[0] 161 | ebitenutil.DrawLine(d.offscreen, p.X, p.Y, next.X, next.Y, colornames.Red) 162 | } 163 | } 164 | } 165 | }) 166 | 167 | camera := archetype.MustFindCamera(w) 168 | cameraPos := transform.Transform.Get(camera).LocalPosition 169 | op := &ebiten.DrawImageOptions{} 170 | op.GeoM.Translate(-cameraPos.X, -cameraPos.Y) 171 | screen.DrawImage(d.offscreen, op) 172 | } 173 | -------------------------------------------------------------------------------- /system/despawn.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/archetype" 9 | "github.com/m110/airplanes/component" 10 | ) 11 | 12 | type Despawn struct { 13 | query *donburi.Query 14 | game *component.GameData 15 | } 16 | 17 | func NewDespawn() *Despawn { 18 | return &Despawn{ 19 | query: donburi.NewQuery(filter.Contains(component.Despawnable)), 20 | } 21 | } 22 | 23 | func (d *Despawn) Update(w donburi.World) { 24 | if d.game == nil { 25 | d.game = component.MustFindGame(w) 26 | if d.game == nil { 27 | return 28 | } 29 | } 30 | 31 | cameraPos := transform.Transform.Get(archetype.MustFindCamera(w)).LocalPosition 32 | 33 | d.query.Each(w, func(entry *donburi.Entry) { 34 | position := transform.Transform.Get(entry).LocalPosition 35 | sprite := component.Sprite.Get(entry) 36 | despawnable := component.Despawnable.Get(entry) 37 | 38 | maxX := position.X + float64(sprite.Image.Bounds().Dx()) 39 | maxY := position.Y + float64(sprite.Image.Bounds().Dy()) 40 | 41 | cameraMaxY := cameraPos.Y + float64(d.game.Settings.ScreenHeight) 42 | cameraMaxX := cameraPos.X + float64(d.game.Settings.ScreenWidth) 43 | 44 | if !despawnable.Spawned { 45 | if position.Y > cameraPos.Y && maxY < cameraMaxY && 46 | position.X > cameraPos.X && maxX < cameraMaxX { 47 | despawnable.Spawned = true 48 | } 49 | 50 | return 51 | } 52 | 53 | if maxY < cameraPos.Y || position.Y > cameraMaxY || 54 | maxX < cameraPos.X || position.X > cameraMaxX { 55 | component.Destroy(entry) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /system/destroy.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type Destroy struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewDestroy() *Destroy { 16 | return &Destroy{ 17 | query: donburi.NewQuery( 18 | filter.Contains(component.Destroyed), 19 | ), 20 | } 21 | } 22 | 23 | func (d *Destroy) Init(w donburi.World) {} 24 | 25 | func (d *Destroy) Update(w donburi.World) { 26 | d.query.Each(w, func(entry *donburi.Entry) { 27 | transform.RemoveRecursive(entry) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /system/events.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/yohamta/donburi" 7 | "github.com/yohamta/donburi/features/events" 8 | "github.com/yohamta/donburi/features/transform" 9 | 10 | "github.com/m110/airplanes/archetype" 11 | "github.com/m110/airplanes/component" 12 | ) 13 | 14 | type EnemyKilled struct { 15 | Enemy *donburi.Entry 16 | } 17 | 18 | var EnemyKilledEvent = events.NewEventType[EnemyKilled]() 19 | 20 | func OnEnemyKilledWreck(w donburi.World, event EnemyKilled) { 21 | if event.Enemy.Valid() && event.Enemy.HasComponent(component.Wreckable) { 22 | archetype.NewAirplaneWreck(w, event.Enemy, component.Sprite.Get(event.Enemy)) 23 | } 24 | } 25 | 26 | func OnEnemyKilledAddScore(w donburi.World, event EnemyKilled) { 27 | component.MustFindGame(w).AddScore(1) 28 | } 29 | 30 | func OnEnemyKilledSpawnCollectible(w donburi.World, event EnemyKilled) { 31 | if !event.Enemy.Valid() { 32 | return 33 | } 34 | 35 | r := rand.Intn(10) 36 | if r < 2 { 37 | archetype.NewRandomCollectible(w, transform.Transform.Get(event.Enemy).LocalPosition) 38 | } 39 | } 40 | 41 | func OnEnemyKilledDestroyEnemy(w donburi.World, event EnemyKilled) { 42 | component.Destroy(event.Enemy) 43 | } 44 | 45 | func SetupEvents(w donburi.World) { 46 | EnemyKilledEvent.Subscribe(w, OnEnemyKilledWreck) 47 | EnemyKilledEvent.Subscribe(w, OnEnemyKilledAddScore) 48 | EnemyKilledEvent.Subscribe(w, OnEnemyKilledSpawnCollectible) 49 | EnemyKilledEvent.Subscribe(w, OnEnemyKilledDestroyEnemy) 50 | } 51 | 52 | type Events struct{} 53 | 54 | func NewEvents() *Events { 55 | return &Events{} 56 | } 57 | 58 | func (e *Events) Update(w donburi.World) { 59 | EnemyKilledEvent.ProcessEvents(w) 60 | } 61 | -------------------------------------------------------------------------------- /system/evolution.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/yohamta/donburi" 6 | dmath "github.com/yohamta/donburi/features/math" 7 | "github.com/yohamta/donburi/features/transform" 8 | "github.com/yohamta/donburi/filter" 9 | 10 | "github.com/m110/airplanes/archetype" 11 | "github.com/m110/airplanes/assets" 12 | "github.com/m110/airplanes/component" 13 | ) 14 | 15 | type Evolution struct { 16 | query *donburi.Query 17 | 18 | shadowBuffer *ebiten.Image 19 | } 20 | 21 | func NewEvolution() *Evolution { 22 | return &Evolution{ 23 | query: donburi.NewQuery(filter.Contains(component.Evolution)), 24 | // TODO Not that universal in terms of size 25 | shadowBuffer: ebiten.NewImage(assets.AirplanesBlue[0].Size()), 26 | } 27 | } 28 | 29 | func (s *Evolution) Update(w donburi.World) { 30 | // TODO Handle player evolving while already evolving (queue evolutions) 31 | s.query.Each(w, func(entry *donburi.Entry) { 32 | evolution := component.Evolution.Get(entry) 33 | if !evolution.Evolving { 34 | return 35 | } 36 | 37 | currentEvolution, _ := transform.FindChildWithComponent(entry, component.CurrentEvolutionTag) 38 | nextEvolution, _ := transform.FindChildWithComponent(entry, component.NextEvolutionTag) 39 | 40 | currentEvolutionSprite := component.Sprite.Get(currentEvolution) 41 | nextEvolutionSprite := component.Sprite.Get(nextEvolution) 42 | 43 | currentEvolutionTransform := transform.Transform.Get(currentEvolution) 44 | nextEvolutionTransform := transform.Transform.Get(nextEvolution) 45 | 46 | shadow, _ := transform.FindChildWithComponent(entry, component.ShadowTag) 47 | shadowSprite := component.Sprite.Get(shadow) 48 | 49 | sprite := component.Sprite.Get(entry) 50 | 51 | if !evolution.StartedEvolving { 52 | // Hide sprite 53 | sprite.Hide() 54 | 55 | // Show evolutions instead 56 | currentEvolutionTransform.LocalScale = dmath.NewVec2(1, 1) 57 | currentEvolutionSprite.Image = whiteImageFromImage(sprite.Image) 58 | currentEvolutionSprite.Show() 59 | 60 | nextEvolutionTransform.LocalScale = dmath.NewVec2(0, 0) 61 | nextEvolutionSprite.Image = whiteImageFromImage(archetype.AirplaneImageByFaction(component.PlayerAirplane.Get(entry).Faction, evolution.Level)) 62 | nextEvolutionSprite.Show() 63 | 64 | evolution.StartedEvolving = true 65 | } 66 | 67 | evolution.GrowTimer.Update() 68 | 69 | nextEvolutionTransform.LocalScale = dmath.NewVec2( 70 | evolution.GrowTimer.PercentDone(), 71 | evolution.GrowTimer.PercentDone(), 72 | ) 73 | 74 | if evolution.GrowTimer.IsReady() { 75 | evolution.ShrinkTimer.Update() 76 | 77 | currentEvolutionTransform.LocalScale = dmath.NewVec2( 78 | 1.0-evolution.ShrinkTimer.PercentDone(), 79 | 1.0-evolution.ShrinkTimer.PercentDone(), 80 | ) 81 | } 82 | 83 | w, h := sprite.Image.Size() 84 | halfW, halfH := float64(w)/2, float64(h)/2 85 | 86 | s.shadowBuffer.Clear() 87 | op := &ebiten.DrawImageOptions{} 88 | op.GeoM.Translate(-halfW, -halfH) 89 | op.GeoM.Scale(currentEvolutionTransform.LocalScale.X, currentEvolutionTransform.LocalScale.Y) 90 | op.GeoM.Translate(halfW, halfH) 91 | s.shadowBuffer.DrawImage(currentEvolutionSprite.Image, op) 92 | 93 | op = &ebiten.DrawImageOptions{} 94 | op.GeoM.Translate(-halfW, -halfH) 95 | op.GeoM.Scale(nextEvolutionTransform.LocalScale.X, nextEvolutionTransform.LocalScale.Y) 96 | op.GeoM.Translate(halfW, halfH) 97 | s.shadowBuffer.DrawImage(nextEvolutionSprite.Image, op) 98 | 99 | shadowSprite.Image.Clear() 100 | op = &ebiten.DrawImageOptions{} 101 | archetype.ShadowDrawOptions(op) 102 | shadowSprite.Image.DrawImage(s.shadowBuffer, op) 103 | 104 | if evolution.ShrinkTimer.IsReady() { 105 | evolution.StopEvolving() 106 | 107 | // Hide evolutions 108 | currentEvolutionSprite.Hide() 109 | nextEvolutionSprite.Hide() 110 | 111 | // Show sprite and shadow 112 | sprite.Image = archetype.AirplaneImageByFaction(component.PlayerAirplane.Get(entry).Faction, evolution.Level) 113 | sprite.Show() 114 | shadowSprite.Image = archetype.ShadowImage(sprite.Image) 115 | } 116 | }) 117 | } 118 | 119 | func whiteImageFromImage(src *ebiten.Image) *ebiten.Image { 120 | img := ebiten.NewImage(src.Size()) 121 | op := &ebiten.DrawImageOptions{} 122 | op.ColorM.Scale(0, 0, 0, 1) 123 | op.ColorM.Translate(1, 1, 1, 0) 124 | img.DrawImage(src, op) 125 | return img 126 | } 127 | -------------------------------------------------------------------------------- /system/follower.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type Follower struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewFollower() *Follower { 16 | return &Follower{ 17 | query: donburi.NewQuery(filter.Contains(transform.Transform, component.Follower)), 18 | } 19 | } 20 | 21 | func (s *Follower) Update(w donburi.World) { 22 | s.query.Each(w, func(entry *donburi.Entry) { 23 | follower := component.Follower.Get(entry) 24 | if follower.Target == nil || !follower.Target.Valid() { 25 | return 26 | } 27 | 28 | follower.FollowingTimer.Update() 29 | if follower.FollowingTimer.IsReady() { 30 | follower.Target = nil 31 | return 32 | } 33 | 34 | // TODO: Should rather rotate towards the target instead of looking at it straight away. 35 | targetPos := transform.WorldPosition(follower.Target) 36 | transform.LookAt(entry, targetPos) 37 | component.Velocity.Get(entry).Velocity = transform.Right(entry).MulScalar(follower.FollowingSpeed) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /system/health.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type Health struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewHealth() *Health { 16 | return &Health{ 17 | query: donburi.NewQuery(filter.Contains( 18 | transform.Transform, 19 | component.Health, 20 | )), 21 | } 22 | } 23 | 24 | func (h *Health) Update(w donburi.World) { 25 | h.query.Each(w, func(entry *donburi.Entry) { 26 | health := component.Health.Get(entry) 27 | 28 | if health.JustDamaged { 29 | health.DamageIndicatorTimer.Update() 30 | if health.DamageIndicatorTimer.IsReady() { 31 | health.HideDamageIndicator() 32 | } 33 | } else { 34 | if health.Health <= 0 { 35 | EnemyKilledEvent.Publish(w, EnemyKilled{ 36 | Enemy: entry, 37 | }) 38 | } 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /system/hud.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/text" 8 | "github.com/yohamta/donburi" 9 | "github.com/yohamta/donburi/filter" 10 | "golang.org/x/image/colornames" 11 | 12 | "github.com/m110/airplanes/assets" 13 | "github.com/m110/airplanes/component" 14 | ) 15 | 16 | type HUD struct { 17 | query *donburi.Query 18 | game *component.GameData 19 | shadowOverlay *ebiten.Image 20 | } 21 | 22 | func NewHUD() *HUD { 23 | return &HUD{ 24 | query: donburi.NewQuery(filter.Contains(component.Player)), 25 | } 26 | } 27 | 28 | func (h *HUD) Draw(w donburi.World, screen *ebiten.Image) { 29 | if h.game == nil { 30 | h.game = component.MustFindGame(w) 31 | if h.game == nil { 32 | return 33 | } 34 | // TODO I don't really like that it's done here 35 | h.shadowOverlay = ebiten.NewImage(h.game.Settings.ScreenWidth, h.game.Settings.ScreenHeight) 36 | h.shadowOverlay.Fill(colornames.Black) 37 | } 38 | 39 | h.query.Each(w, func(entry *donburi.Entry) { 40 | player := component.Player.Get(entry) 41 | 42 | icon := assets.Health 43 | iconWidth, iconHeight := icon.Size() 44 | 45 | baseY := float64(h.game.Settings.ScreenHeight) - float64(iconHeight) - 5 46 | var baseX float64 47 | switch player.PlayerNumber { 48 | case 1: 49 | baseX = 5 50 | case 2: 51 | baseX = float64(h.game.Settings.ScreenWidth) - 5 - float64(iconWidth) 52 | } 53 | 54 | op := &ebiten.DrawImageOptions{} 55 | op.GeoM.Translate(baseX, baseY) 56 | for i := 0; i < player.Lives; i++ { 57 | if i > 0 { 58 | op.GeoM.Translate(0, -float64(iconHeight+2)) 59 | } 60 | screen.DrawImage(icon, op) 61 | } 62 | }) 63 | 64 | if h.game.GameOver { 65 | op := &ebiten.DrawImageOptions{} 66 | op.ColorM.Scale(0, 0, 0, 0.5) 67 | screen.DrawImage(h.shadowOverlay, op) 68 | 69 | text.Draw( 70 | screen, 71 | "GAME OVER", 72 | assets.NormalFont, 73 | h.game.Settings.ScreenWidth/4+20, 74 | h.game.Settings.ScreenHeight/2, 75 | colornames.White, 76 | ) 77 | } else if h.game.Paused { 78 | op := &ebiten.DrawImageOptions{} 79 | op.ColorM.Scale(0, 0, 0, 0.5) 80 | screen.DrawImage(h.shadowOverlay, op) 81 | 82 | text.Draw( 83 | screen, 84 | "PAUSED", 85 | assets.NormalFont, 86 | h.game.Settings.ScreenWidth/4+40, 87 | h.game.Settings.ScreenHeight/2, 88 | colornames.White, 89 | ) 90 | } 91 | 92 | text.Draw(screen, fmt.Sprintf("Score: %06d", h.game.Score), assets.NormalFont, h.game.Settings.ScreenWidth/4, 30, colornames.White) 93 | } 94 | -------------------------------------------------------------------------------- /system/invulnerable.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/filter" 6 | 7 | "github.com/m110/airplanes/component" 8 | ) 9 | 10 | type Invulnerable struct { 11 | query *donburi.Query 12 | } 13 | 14 | func NewInvulnerable() *Invulnerable { 15 | return &Invulnerable{ 16 | query: donburi.NewQuery(filter.Contains(component.PlayerAirplane)), 17 | } 18 | } 19 | 20 | func (s *Invulnerable) Update(w donburi.World) { 21 | s.query.Each(w, func(entry *donburi.Entry) { 22 | player := component.PlayerAirplane.Get(entry) 23 | if player.Invulnerable { 24 | player.InvulnerableTimer.Update() 25 | if player.InvulnerableTimer.IsReady() { 26 | player.StopInvulnerability() 27 | } 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /system/label.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/hajimehoshi/ebiten/v2/text" 6 | "github.com/yohamta/donburi" 7 | "github.com/yohamta/donburi/features/transform" 8 | "github.com/yohamta/donburi/filter" 9 | "golang.org/x/image/colornames" 10 | 11 | "github.com/m110/airplanes/assets" 12 | "github.com/m110/airplanes/component" 13 | ) 14 | 15 | type Label struct { 16 | query *donburi.Query 17 | } 18 | 19 | func NewLabel() *Label { 20 | return &Label{ 21 | query: donburi.NewQuery( 22 | filter.Contains(transform.Transform, component.Label), 23 | ), 24 | } 25 | } 26 | 27 | func (l *Label) Draw(w donburi.World, screen *ebiten.Image) { 28 | l.query.Each(w, func(entry *donburi.Entry) { 29 | label := component.Label.Get(entry) 30 | if label.Hidden { 31 | return 32 | } 33 | 34 | pos := transform.WorldPosition(entry) 35 | 36 | // TODO Rotation, Scale, customizable font and color 37 | text.Draw( 38 | screen, 39 | label.Text, 40 | assets.SmallFont, 41 | int(pos.X), 42 | int(pos.Y), 43 | colornames.White, 44 | ) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /system/maxdistance.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type DistanceLimit struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewDistanceLimit() *DistanceLimit { 16 | return &DistanceLimit{ 17 | query: donburi.NewQuery( 18 | filter.Contains(component.DistanceLimit), 19 | ), 20 | } 21 | } 22 | 23 | func (l *DistanceLimit) Init(w donburi.World) {} 24 | 25 | func (l *DistanceLimit) Update(w donburi.World) { 26 | l.query.Each(w, func(entry *donburi.Entry) { 27 | dl := component.DistanceLimit.Get(entry) 28 | pos := transform.WorldPosition(entry) 29 | 30 | if !dl.Initialized { 31 | dl.Initialized = true 32 | dl.PreviousPosition = pos 33 | return 34 | } 35 | 36 | distance := pos.Distance(dl.PreviousPosition) 37 | dl.DistanceTraveled += distance 38 | 39 | if dl.DistanceTraveled > dl.MaxDistance { 40 | component.Destroy(entry) 41 | } else { 42 | dl.PreviousPosition = pos 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /system/observer.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type Observer struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewObserver() *Observer { 16 | return &Observer{ 17 | query: donburi.NewQuery(filter.Contains(transform.Transform, component.Observer)), 18 | } 19 | } 20 | 21 | func (s *Observer) Update(w donburi.World) { 22 | s.query.Each(w, func(entry *donburi.Entry) { 23 | observer := component.Observer.Get(entry) 24 | if observer.LookFor == nil { 25 | return 26 | } 27 | 28 | observer.Target = component.ClosestTarget(w, entry, observer.LookFor) 29 | if observer.Target == nil { 30 | return 31 | } 32 | 33 | // TODO: Should rather rotate towards the target instead of looking at it straight away. 34 | targetPos := transform.WorldPosition(observer.Target) 35 | transform.LookAt(entry, targetPos) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /system/playerselect.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/hajimehoshi/ebiten/v2/inpututil" 9 | "github.com/yohamta/donburi" 10 | "github.com/yohamta/donburi/features/hierarchy" 11 | "github.com/yohamta/donburi/features/transform" 12 | "github.com/yohamta/donburi/filter" 13 | 14 | "github.com/m110/airplanes/archetype" 15 | "github.com/m110/airplanes/component" 16 | "github.com/m110/airplanes/engine" 17 | ) 18 | 19 | type ChosenPlayer struct { 20 | PlayerNumber int 21 | Faction component.PlayerFaction 22 | } 23 | 24 | type StartGameCallback func(players []ChosenPlayer) 25 | 26 | type PlayerSelect struct { 27 | query *donburi.Query 28 | startCallback StartGameCallback 29 | backToMenuCallback func() 30 | 31 | started bool 32 | altitudeTimer *engine.Timer 33 | chosenPlayers []ChosenPlayer 34 | } 35 | 36 | func NewPlayerSelect(startCallback StartGameCallback, backToMenuCallback func()) *PlayerSelect { 37 | return &PlayerSelect{ 38 | query: donburi.NewQuery( 39 | filter.Contains( 40 | transform.Transform, 41 | component.PlayerSelect, 42 | component.Velocity, 43 | component.Altitude, 44 | ), 45 | ), 46 | startCallback: startCallback, 47 | backToMenuCallback: backToMenuCallback, 48 | altitudeTimer: engine.NewTimer(time.Second), 49 | } 50 | } 51 | 52 | func (s *PlayerSelect) Update(w donburi.World) { 53 | if s.started { 54 | s.query.Each(w, func(entry *donburi.Entry) { 55 | playerSelect := component.PlayerSelect.Get(entry) 56 | if !playerSelect.Selected || !playerSelect.Ready { 57 | return 58 | } 59 | 60 | velocity := component.Velocity.Get(entry) 61 | velocity.Velocity.Y -= 0.01 62 | 63 | s.altitudeTimer.Update() 64 | if s.altitudeTimer.IsReady() { 65 | component.Altitude.Get(entry).Velocity = 0.005 66 | } 67 | 68 | // TODO dynamic sprite size not hardcoded 69 | if transform.WorldPosition(entry).Y <= -32 { 70 | s.startCallback(s.chosenPlayers) 71 | } 72 | }) 73 | 74 | return 75 | } 76 | 77 | var playerSelects []*donburi.Entry 78 | selected := map[int]*donburi.Entry{} 79 | s.query.Each(w, func(entry *donburi.Entry) { 80 | playerSelect := component.PlayerSelect.Get(entry) 81 | if playerSelect.Selected { 82 | selected[playerSelect.PlayerNumber] = entry 83 | } 84 | 85 | playerSelects = append(playerSelects, entry) 86 | }) 87 | 88 | var isTouch bool 89 | touchIDs := inpututil.AppendJustPressedTouchIDs(nil) 90 | if len(touchIDs) > 0 { 91 | isTouch = true 92 | } 93 | 94 | for number, settings := range archetype.Players { 95 | if inpututil.IsKeyJustPressed(settings.Inputs.Shoot) || isTouch { 96 | if entry, ok := selected[number]; ok { 97 | component.PlayerSelect.Get(entry).LockIn() 98 | } else { 99 | for _, entry := range playerSelects { 100 | playerSelect := component.PlayerSelect.Get(entry) 101 | if !playerSelect.Selected { 102 | playerSelect.Select(number) 103 | break 104 | } 105 | } 106 | } 107 | } 108 | 109 | if inpututil.IsKeyJustPressed(settings.Inputs.Left) { 110 | if entry, ok := selected[number]; ok { 111 | playerSelect := component.PlayerSelect.Get(entry) 112 | if !playerSelect.Ready { 113 | // TODO refactor 114 | if playerSelect.Index > 0 { 115 | for i := playerSelect.Index - 1; i >= 0; i-- { 116 | entry := playerSelects[i] 117 | ps := component.PlayerSelect.Get(entry) 118 | if !ps.Selected { 119 | playerSelect.Unselect() 120 | 121 | ps.Select(number) 122 | break 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | if inpututil.IsKeyJustPressed(settings.Inputs.Right) { 131 | if entry, ok := selected[number]; ok { 132 | playerSelect := component.PlayerSelect.Get(entry) 133 | if !playerSelect.Ready { 134 | if playerSelect.Index < len(playerSelects)-1 { 135 | for _, entry := range playerSelects[playerSelect.Index+1:] { 136 | ps := component.PlayerSelect.Get(entry) 137 | if !ps.Selected { 138 | playerSelect.Unselect() 139 | 140 | ps.Select(number) 141 | break 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | // TODO Cancel just the last action 151 | if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { 152 | cancelled := false 153 | for _, entry := range playerSelects { 154 | playerSelect := component.PlayerSelect.Get(entry) 155 | if playerSelect.Ready { 156 | playerSelect.Release() 157 | cancelled = true 158 | } 159 | if playerSelect.Selected { 160 | playerSelect.Unselect() 161 | cancelled = true 162 | } 163 | } 164 | 165 | if !cancelled { 166 | s.backToMenuCallback() 167 | } 168 | } 169 | 170 | for _, entry := range playerSelects { 171 | playerSelect := component.PlayerSelect.Get(entry) 172 | crosshair := hierarchy.MustGetChildren(entry)[0] 173 | label := hierarchy.MustGetChildren(crosshair)[0] 174 | 175 | if playerSelect.Selected { 176 | if playerSelect.Ready { 177 | component.Sprite.Get(crosshair).Hidden = true 178 | } else { 179 | component.Sprite.Get(crosshair).Hidden = false 180 | } 181 | 182 | component.Label.Get(label).Text = fmt.Sprintf("Player %v", playerSelect.PlayerNumber) 183 | component.Label.Get(label).Hidden = false 184 | } else { 185 | component.Sprite.Get(crosshair).Hidden = true 186 | component.Label.Get(label).Hidden = true 187 | } 188 | } 189 | 190 | var chosenPlayers []ChosenPlayer 191 | playersReady := 0 192 | for _, entry := range playerSelects { 193 | playerSelect := component.PlayerSelect.Get(entry) 194 | if playerSelect.Selected { 195 | chosenPlayers = append(chosenPlayers, ChosenPlayer{ 196 | PlayerNumber: playerSelect.PlayerNumber, 197 | Faction: playerSelect.Faction, 198 | }) 199 | if playerSelect.Ready { 200 | playersReady++ 201 | } 202 | } 203 | } 204 | 205 | if playersReady > 0 && playersReady == len(chosenPlayers) { 206 | s.chosenPlayers = chosenPlayers 207 | s.started = true 208 | for _, entry := range playerSelects { 209 | ps := component.PlayerSelect.Get(entry) 210 | if ps.Selected && ps.Ready { 211 | velocity := component.Velocity.Get(entry) 212 | velocity.Velocity.Y = -0.5 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /system/progression.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/math" 6 | "github.com/yohamta/donburi/features/transform" 7 | "github.com/yohamta/donburi/filter" 8 | 9 | "github.com/m110/airplanes/archetype" 10 | "github.com/m110/airplanes/component" 11 | ) 12 | 13 | type Progression struct { 14 | query *donburi.Query 15 | nextLevelFunc func() 16 | } 17 | 18 | func NewProgression(nextLevelFunc func()) *Progression { 19 | return &Progression{ 20 | query: donburi.NewQuery( 21 | filter.Contains( 22 | component.PlayerAirplane, 23 | component.Velocity, 24 | component.Input, 25 | component.Bounds, 26 | ), 27 | ), 28 | nextLevelFunc: nextLevelFunc, 29 | } 30 | } 31 | 32 | func (p *Progression) Update(w donburi.World) { 33 | levelEntry := component.MustFindLevel(w) 34 | level := component.Level.Get(levelEntry) 35 | 36 | if level.Progressed { 37 | cameraPos := transform.Transform.Get(archetype.MustFindCamera(w)).LocalPosition 38 | playersVisible := false 39 | p.query.Each(w, func(entry *donburi.Entry) { 40 | playerPos := transform.Transform.Get(entry).LocalPosition 41 | playerSprite := component.Sprite.Get(entry) 42 | if playerPos.Y+float64(playerSprite.Image.Bounds().Dy()) > cameraPos.Y { 43 | playersVisible = true 44 | } 45 | }) 46 | if !playersVisible { 47 | p.nextLevelFunc() 48 | } 49 | return 50 | } 51 | 52 | if level.ReachedEnd { 53 | level.ProgressionTimer.Update() 54 | if level.ProgressionTimer.IsReady() { 55 | p.query.Each(w, func(entry *donburi.Entry) { 56 | input := component.Input.Get(entry) 57 | input.Disabled = true 58 | 59 | velocity := component.Velocity.Get(entry) 60 | velocity.Velocity = math.Vec2{ 61 | X: 0, 62 | Y: -3, 63 | } 64 | 65 | bounds := component.Bounds.Get(entry) 66 | bounds.Disabled = true 67 | }) 68 | 69 | level.Progressed = true 70 | } 71 | } else { 72 | camera := archetype.MustFindCamera(w) 73 | 74 | cameraPos := transform.Transform.Get(camera).LocalPosition 75 | if cameraPos.Y == 0 { 76 | level.ReachedEnd = true 77 | level.ProgressionTimer.Reset() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /system/render.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/samber/lo" 9 | "github.com/yohamta/donburi" 10 | "github.com/yohamta/donburi/features/transform" 11 | "github.com/yohamta/donburi/filter" 12 | 13 | "github.com/m110/airplanes/archetype" 14 | "github.com/m110/airplanes/component" 15 | ) 16 | 17 | type Render struct { 18 | query *donburi.Query 19 | uiQuery *donburi.Query 20 | offscreen *ebiten.Image 21 | debug *component.DebugData 22 | } 23 | 24 | func NewRenderer() *Render { 25 | return &Render{ 26 | query: donburi.NewQuery( 27 | filter.And( 28 | filter.Contains(transform.Transform, component.Sprite), 29 | filter.Not(filter.Contains(component.UI)), 30 | ), 31 | ), 32 | uiQuery: donburi.NewQuery( 33 | filter.Contains(transform.Transform, component.Sprite, component.UI), 34 | ), 35 | // TODO figure out the proper size 36 | offscreen: ebiten.NewImage(3000, 3000), 37 | } 38 | } 39 | 40 | func (r *Render) Update(w donburi.World) { 41 | if r.debug == nil { 42 | debug, ok := donburi.NewQuery(filter.Contains(component.Debug)).First(w) 43 | if !ok { 44 | return 45 | } 46 | 47 | r.debug = component.Debug.Get(debug) 48 | } 49 | } 50 | 51 | func (r *Render) Draw(w donburi.World, screen *ebiten.Image) { 52 | camera := archetype.MustFindCamera(w) 53 | cameraPos := transform.Transform.Get(camera).LocalPosition 54 | 55 | r.offscreen.Clear() 56 | 57 | var entries []*donburi.Entry 58 | r.query.Each(w, func(entry *donburi.Entry) { 59 | entries = append(entries, entry) 60 | }) 61 | 62 | byLayer := lo.GroupBy(entries, func(entry *donburi.Entry) int { 63 | return int(component.Sprite.Get(entry).Layer) 64 | }) 65 | layers := lo.Keys(byLayer) 66 | sort.Ints(layers) 67 | 68 | for _, layer := range layers { 69 | for _, entry := range byLayer[layer] { 70 | sprite := component.Sprite.Get(entry) 71 | 72 | if sprite.Hidden { 73 | continue 74 | } 75 | 76 | w, h := sprite.Image.Size() 77 | halfW, halfH := float64(w)/2, float64(h)/2 78 | 79 | op := &ebiten.DrawImageOptions{} 80 | op.GeoM.Translate(-halfW, -halfH) 81 | op.GeoM.Rotate(float64(int(transform.WorldRotation(entry)-sprite.OriginalRotation)%360) * 2 * math.Pi / 360) 82 | op.GeoM.Translate(halfW, halfH) 83 | 84 | position := transform.WorldPosition(entry) 85 | 86 | x := position.X 87 | y := position.Y 88 | 89 | switch sprite.Pivot { 90 | case component.SpritePivotCenter: 91 | x -= halfW 92 | y -= halfH 93 | } 94 | 95 | scale := transform.WorldScale(entry) 96 | op.GeoM.Translate(-halfW, -halfH) 97 | op.GeoM.Scale(scale.X, scale.Y) 98 | op.GeoM.Translate(halfW, halfH) 99 | 100 | if sprite.ColorOverride != nil { 101 | op.ColorM.Scale(0, 0, 0, sprite.ColorOverride.A) 102 | op.ColorM.Translate(sprite.ColorOverride.R, sprite.ColorOverride.G, sprite.ColorOverride.B, 0) 103 | } 104 | 105 | op.GeoM.Translate(x, y) 106 | 107 | r.offscreen.DrawImage(sprite.Image, op) 108 | } 109 | } 110 | 111 | op := &ebiten.DrawImageOptions{} 112 | op.GeoM.Translate(-cameraPos.X, -cameraPos.Y) 113 | screen.DrawImage(r.offscreen, op) 114 | 115 | // TODO deduplicate 116 | r.uiQuery.Each(w, func(entry *donburi.Entry) { 117 | sprite := component.Sprite.Get(entry) 118 | 119 | if sprite.Hidden { 120 | return 121 | } 122 | 123 | w, h := sprite.Image.Size() 124 | halfW, halfH := float64(w)/2, float64(h)/2 125 | 126 | op := &ebiten.DrawImageOptions{} 127 | op.GeoM.Translate(-halfW, -halfH) 128 | op.GeoM.Rotate(float64(int(transform.WorldRotation(entry)-sprite.OriginalRotation)%360) * 2 * math.Pi / 360) 129 | op.GeoM.Translate(halfW, halfH) 130 | 131 | position := transform.WorldPosition(entry) 132 | 133 | x := position.X 134 | y := position.Y 135 | 136 | switch sprite.Pivot { 137 | case component.SpritePivotCenter: 138 | x -= halfW 139 | y -= halfH 140 | } 141 | 142 | scale := transform.WorldScale(entry) 143 | op.GeoM.Translate(-halfW, -halfH) 144 | op.GeoM.Scale(scale.X, scale.Y) 145 | op.GeoM.Translate(halfW, halfH) 146 | 147 | op.GeoM.Translate(x, y) 148 | 149 | screen.DrawImage(sprite.Image, op) 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /system/respawn.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/hajimehoshi/ebiten/v2/inpututil" 6 | "github.com/yohamta/donburi" 7 | "github.com/yohamta/donburi/filter" 8 | 9 | "github.com/m110/airplanes/archetype" 10 | "github.com/m110/airplanes/component" 11 | ) 12 | 13 | type Respawn struct { 14 | query *donburi.Query 15 | game *component.GameData 16 | restartCallback func() 17 | } 18 | 19 | func NewRespawn(restartCallback func()) *Respawn { 20 | return &Respawn{ 21 | query: donburi.NewQuery(filter.Contains(component.Player)), 22 | restartCallback: restartCallback, 23 | } 24 | } 25 | 26 | func (r *Respawn) Update(w donburi.World) { 27 | if r.game == nil { 28 | r.game = component.MustFindGame(w) 29 | if r.game == nil { 30 | return 31 | } 32 | } 33 | 34 | if r.game.GameOver { 35 | if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) { 36 | r.restartCallback() 37 | } 38 | return 39 | } 40 | 41 | playersAlive := 0 42 | 43 | r.query.Each(w, func(entry *donburi.Entry) { 44 | player := component.Player.Get(entry) 45 | 46 | if player.Lives > 0 { 47 | playersAlive++ 48 | } 49 | 50 | if player.Respawning { 51 | player.RespawnTimer.Update() 52 | if player.RespawnTimer.IsReady() { 53 | player.Respawning = false 54 | archetype.NewPlayerAirplane(w, *player, player.PlayerFaction, player.EvolutionLevel()) 55 | } 56 | } 57 | }) 58 | 59 | // TODO Is this the proper system? 60 | if playersAlive == 0 { 61 | game := component.MustFindGame(w) 62 | if !game.GameOver { 63 | game.GameOver = true 64 | cam := archetype.MustFindCamera(w) 65 | component.Velocity.Get(cam).Velocity.Y = 0 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /system/script.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/filter" 6 | 7 | "github.com/m110/airplanes/component" 8 | ) 9 | 10 | type Script struct { 11 | query *donburi.Query 12 | } 13 | 14 | func NewScript() *Script { 15 | return &Script{ 16 | query: donburi.NewQuery(filter.Contains(component.Script)), 17 | } 18 | } 19 | 20 | func (s *Script) Update(w donburi.World) { 21 | s.query.Each(w, func(entry *donburi.Entry) { 22 | script := component.Script.Get(entry) 23 | script.Update(w) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /system/shooter.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/archetype" 9 | "github.com/m110/airplanes/component" 10 | ) 11 | 12 | type Shooter struct { 13 | query *donburi.Query 14 | } 15 | 16 | func NewShooter() *Shooter { 17 | return &Shooter{ 18 | query: donburi.NewQuery(filter.Contains(transform.Transform, component.Shooter)), 19 | } 20 | } 21 | 22 | func (s *Shooter) Update(w donburi.World) { 23 | s.query.Each(w, func(entry *donburi.Entry) { 24 | shooter := component.Shooter.Get(entry) 25 | 26 | shooter.ShootTimer.Update() 27 | 28 | // TODO: It feels like a hack. 29 | // This relies on another system and requires it to be running before this one. 30 | // Could be merged into one system, however having them separately also makes sense. 31 | // Perhaps both components be used by the AI system? 32 | if entry.HasComponent(component.Observer) { 33 | observer := component.Observer.Get(entry) 34 | if observer.Target == nil { 35 | return 36 | } 37 | } 38 | 39 | if shooter.ShootTimer.IsReady() { 40 | shooter.ShootTimer.Reset() 41 | 42 | switch shooter.Type { 43 | case component.ShooterTypeBullet: 44 | archetype.NewEnemyBullet(w, transform.WorldPosition(entry), transform.WorldRotation(entry)) 45 | case component.ShooterTypeMissile: 46 | archetype.NewEnemyMissile(w, transform.WorldPosition(entry), transform.WorldRotation(entry)) 47 | case component.ShooterTypeBeam: 48 | } 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /system/spawn.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/archetype" 9 | "github.com/m110/airplanes/component" 10 | ) 11 | 12 | type Spawn struct { 13 | query *donburi.Query 14 | } 15 | 16 | func NewSpawn() *Spawn { 17 | return &Spawn{ 18 | query: donburi.NewQuery(filter.Contains(component.Spawnable)), 19 | } 20 | } 21 | 22 | func (s *Spawn) Update(w donburi.World) { 23 | cameraPos := transform.WorldPosition(archetype.MustFindCamera(w)) 24 | 25 | s.query.Each(w, func(entry *donburi.Entry) { 26 | t := transform.Transform.Get(entry) 27 | 28 | if cameraPos.Y <= t.LocalPosition.Y { 29 | spawnable := component.Spawnable.Get(entry) 30 | spawnable.SpawnFunc(w) 31 | component.Destroy(entry) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /system/timetolive.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/filter" 6 | 7 | "github.com/m110/airplanes/component" 8 | ) 9 | 10 | type TimeToLive struct { 11 | query *donburi.Query 12 | } 13 | 14 | func NewTimeToLive() *TimeToLive { 15 | return &TimeToLive{ 16 | query: donburi.NewQuery( 17 | filter.Contains(component.TimeToLive), 18 | ), 19 | } 20 | } 21 | 22 | func (t *TimeToLive) Init(w donburi.World) {} 23 | 24 | func (t *TimeToLive) Update(w donburi.World) { 25 | t.query.Each(w, func(entry *donburi.Entry) { 26 | ttl := component.TimeToLive.Get(entry) 27 | ttl.Timer.Update() 28 | if ttl.Timer.IsReady() { 29 | component.Destroy(entry) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /system/velocity.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/yohamta/donburi" 5 | "github.com/yohamta/donburi/features/transform" 6 | "github.com/yohamta/donburi/filter" 7 | 8 | "github.com/m110/airplanes/component" 9 | ) 10 | 11 | type Velocity struct { 12 | query *donburi.Query 13 | } 14 | 15 | func NewVelocity() *Velocity { 16 | return &Velocity{ 17 | query: donburi.NewQuery( 18 | filter.Contains(transform.Transform, component.Velocity), 19 | ), 20 | } 21 | } 22 | 23 | func (v *Velocity) Update(w donburi.World) { 24 | v.query.Each(w, func(entry *donburi.Entry) { 25 | t := transform.Transform.Get(entry) 26 | velocity := component.Velocity.Get(entry) 27 | 28 | t.LocalPosition = t.LocalPosition.Add(velocity.Velocity) 29 | t.LocalRotation += velocity.RotationVelocity 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | --------------------------------------------------------------------------------