├── .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 | [](https://github.com/m110/airplanes/blob/master/LICENSE)
4 | [](https://goreportcard.com/report/github.com/m110/airplanes)
5 | [](https://github.com/m110/airplanes/actions/workflows/deploy-web.yml)
6 | [](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 | 
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 | 
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 | 
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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------