├── screens
├── 1.png
└── level.png
├── svg
├── svg.go
├── layer.go
├── parser.go
├── rect.go
├── ellipse.go
├── path.go
└── parser_test.go
├── assets
├── modal.png
├── paper.jpg
├── hood
│ ├── hood.png
│ └── radar_arrow.png
├── parts
│ ├── box.png
│ ├── leg.png
│ ├── cabin.png
│ ├── cargo.png
│ ├── crane.png
│ ├── engine.png
│ ├── chain_el.png
│ ├── leg_fastening.png
│ ├── crane_lower_jaw.png
│ ├── crane_upper_jaw.png
│ ├── chain_el.svg
│ ├── cargo.svg
│ ├── crane.svg
│ ├── leg.svg
│ ├── engine.svg
│ ├── leg_fastening.svg
│ ├── cabin.svg
│ ├── box.svg
│ ├── crane_lower_jaw.svg
│ └── crane_upper_jaw.svg
├── flame_particle.png
├── ships
│ ├── corvette.yaml
│ ├── ship1.yaml
│ ├── rocket.yaml
│ └── heavy_crane.yaml
└── terrains
│ ├── flat.svg
│ └── long_trip.svg
├── level_test.go
├── ship_test.go
├── tanker.go
├── main.go
├── part.go
├── .gitignore
├── levels
├── 2.yaml
├── 3.yaml
├── 1.yaml
├── 5.yaml
└── 4.yaml
├── errs.go
├── localization.go
├── ebiten_test.go
├── consts.go
├── .github
└── workflows
│ ├── release.yml
│ └── go.yml
├── cam.go
├── background.go
├── go.mod
├── visit_platform_task.go
├── deliver_cargo_task.go
├── modal.go
├── task.go
├── direction.go
├── game_decorator.go
├── cargo.go
├── sprite.go
├── terrain.go
├── README.md
├── utils.go
├── game_obj.go
├── engine.go
├── part_def.go
├── ps.go
├── basic_parts.go
├── crane_jaws.go
├── level.go
├── platform.go
├── world.go
├── init.go
├── level_selector.go
├── crane.go
├── ship.go
├── go.sum
└── game.go
/screens/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/screens/1.png
--------------------------------------------------------------------------------
/svg/svg.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | type Svg struct {
4 | Layers []Layer `xml:"g"`
5 | }
6 |
--------------------------------------------------------------------------------
/assets/modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/modal.png
--------------------------------------------------------------------------------
/assets/paper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/paper.jpg
--------------------------------------------------------------------------------
/screens/level.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/screens/level.png
--------------------------------------------------------------------------------
/assets/hood/hood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/hood/hood.png
--------------------------------------------------------------------------------
/assets/parts/box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/box.png
--------------------------------------------------------------------------------
/assets/parts/leg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/leg.png
--------------------------------------------------------------------------------
/assets/parts/cabin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/cabin.png
--------------------------------------------------------------------------------
/assets/parts/cargo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/cargo.png
--------------------------------------------------------------------------------
/assets/parts/crane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/crane.png
--------------------------------------------------------------------------------
/assets/parts/engine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/engine.png
--------------------------------------------------------------------------------
/assets/flame_particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/flame_particle.png
--------------------------------------------------------------------------------
/assets/parts/chain_el.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/chain_el.png
--------------------------------------------------------------------------------
/assets/hood/radar_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/hood/radar_arrow.png
--------------------------------------------------------------------------------
/level_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestXName(t *testing.T) {
8 | }
9 |
--------------------------------------------------------------------------------
/assets/parts/leg_fastening.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/leg_fastening.png
--------------------------------------------------------------------------------
/assets/parts/crane_lower_jaw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/crane_lower_jaw.png
--------------------------------------------------------------------------------
/assets/parts/crane_upper_jaw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiritofsim/go-space-crane/HEAD/assets/parts/crane_upper_jaw.png
--------------------------------------------------------------------------------
/ship_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestMarshalShipDef(t *testing.T) {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/tanker.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Tank interface {
4 | GetFuel() float64
5 | // TODO: rename
6 | ReduceFuel(val float64)
7 | }
8 |
--------------------------------------------------------------------------------
/svg/layer.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | type Layer struct {
4 | ID string `xml:"id,attr"`
5 | Pathes []Path `xml:"path"`
6 | Rects []Rect `xml:"rect"`
7 | Ellipses []Ellipse `xml:"ellipse"`
8 | }
9 |
--------------------------------------------------------------------------------
/assets/ships/corvette.yaml:
--------------------------------------------------------------------------------
1 | - -
2 | - cab dir=U
3 | -
4 | - - box
5 | - box
6 | - box
7 | - - eng dir=D;pow=150;keys=30,31
8 | -
9 | - eng dir=D;pow=150;keys=29,31
10 |
11 | energy: 100
12 | fuel: 5000
13 | maxfuel: 5000
14 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hajimehoshi/ebiten/v2"
5 | )
6 |
7 | func main() {
8 | g := NewGameDecorator()
9 | checkErr(ebiten.RunGame(g))
10 | }
11 |
12 | func checkErr(err error) {
13 | if err != nil {
14 | panic(err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/assets/ships/ship1.yaml:
--------------------------------------------------------------------------------
1 | - -
2 | - cab dir=U
3 | -
4 | - - eng dir=L;pow=20;keys=30;size=0.5
5 | - box
6 | - eng dir=R;pow=20;keys=29;size=0.5
7 | - - lft dir=R
8 | - box
9 | - lft dir=D
10 | - - leg dir=D
11 | - eng dir=D;pow=100;keys=31
12 | - leg dir=D
13 |
14 | energy: 100
15 | fuel: 5000
16 | maxfuel: 5000
17 |
--------------------------------------------------------------------------------
/part.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | )
7 |
8 | type Part interface {
9 | GetBody() *box2d.B2Body
10 | Update(keys Keys)
11 | Draw(screen *ebiten.Image, cam Cam)
12 | GetVel() float64
13 | GetVelVec() box2d.B2Vec2
14 | GetAng() float64
15 | GetPos() box2d.B2Vec2
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | .idea
18 | *.psd
19 | debug.log
--------------------------------------------------------------------------------
/levels/2.yaml:
--------------------------------------------------------------------------------
1 | name: Rocket
2 | description: |
3 | Try to control heavy rocket
4 | Your task is to visit platform p2
5 | terrain: flat
6 | ship:
7 | name: rocket
8 | energy: 1000
9 | fuel: 10000
10 | maxfuel: 10000
11 | # pos:
12 | # x: 100
13 | # y: 100
14 | platforms:
15 | p1:
16 | fuel: 1000
17 | p2:
18 | fuel: 1000
19 |
20 | tasks:
21 | - "v:p2"
22 |
--------------------------------------------------------------------------------
/levels/3.yaml:
--------------------------------------------------------------------------------
1 | name: Crane
2 | description: |
3 | Try to control crane
4 | You task is to deliver cargo to p2
5 | terrain: flat
6 | ship:
7 | name: heavy_crane
8 | energy: 1000
9 | fuel: 10000
10 | maxfuel: 10000
11 | # pos:
12 | # x: 100
13 | # y: 100
14 | platforms:
15 | p1:
16 | fuel: 1000
17 | p2:
18 | fuel: 1000
19 |
20 | tasks:
21 | - "d:1->p2"
22 |
--------------------------------------------------------------------------------
/assets/ships/rocket.yaml:
--------------------------------------------------------------------------------
1 | - -
2 | - cab dir=U
3 | -
4 | - -
5 | - box
6 | -
7 | - -
8 | - box
9 | -
10 | - -
11 | - box
12 | -
13 | - - lft dir=R
14 | - box
15 | - lft dir=D
16 | - - eng dir=D;pow=150;keys=30,31
17 | - eng dir=D;pow=150;keys=31
18 | - eng dir=D;pow=150;keys=29,31
19 |
20 | energy: 100
21 | fuel: 5000
22 | maxfuel: 5000
23 |
--------------------------------------------------------------------------------
/errs.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "errors"
4 |
5 | var levelComplete = errors.New("level complete")
6 | var levelFailed = errors.New("level failed")
7 |
8 | type SelectedLevel struct {
9 | name string
10 | }
11 |
12 | func NewLevelSelected(name string) *SelectedLevel {
13 | return &SelectedLevel{name: name}
14 | }
15 |
16 | func (ls *SelectedLevel) Error() string {
17 | return ls.name
18 | }
19 |
--------------------------------------------------------------------------------
/levels/1.yaml:
--------------------------------------------------------------------------------
1 | name: Train
2 | description: |
3 | Try to fly on simplest ship
4 | You task is to visit platform p2 and then return back to base p1
5 | terrain: flat
6 | ship:
7 | name: corvette
8 | energy: 1000
9 | fuel: 10000
10 | maxfuel: 10000
11 | # pos:
12 | # x: 100
13 | # y: 100
14 | platforms:
15 | p1:
16 | fuel: 1000
17 | p2:
18 | fuel: 1000
19 |
20 | tasks:
21 | - "v:p2"
22 | - "v:p1"
23 |
24 |
--------------------------------------------------------------------------------
/levels/5.yaml:
--------------------------------------------------------------------------------
1 | name: Train2
2 | description: |
3 | Try to fly on rocket like ship
4 | You task is to visit platform p2 and then return back to base p1
5 | terrain: flat
6 | ship:
7 | name: ship1
8 | energy: 1000
9 | fuel: 10000
10 | maxfuel: 10000
11 | # pos:
12 | # x: 100
13 | # y: 100
14 | platforms:
15 | p1:
16 | fuel: 1000
17 | p2:
18 | fuel: 1000
19 |
20 | tasks:
21 | - "v:p2"
22 | - "v:p1"
23 |
24 |
--------------------------------------------------------------------------------
/levels/4.yaml:
--------------------------------------------------------------------------------
1 | name: Long trip
2 | description: |
3 | Visit all platforms
4 | terrain: long_trip
5 | ship:
6 | name: corvette
7 | energy: 1000
8 | fuel: 10000
9 | maxfuel: 10000
10 | # pos:
11 | # x: 100
12 | # y: 100
13 | #platforms:
14 | # p1:
15 | # fuel: 1000
16 | # p2:
17 | # fuel: 1000
18 |
19 | tasks:
20 | - "v:p1"
21 | - "v:p2"
22 | - "v:p3"
23 | - "v:p4"
24 | - "v:p5"
25 | - "v:p6"
26 | - "v:p7"
27 | - "v:p8"
28 | - "v:p9"
29 |
--------------------------------------------------------------------------------
/assets/ships/heavy_crane.yaml:
--------------------------------------------------------------------------------
1 | - -
2 | -
3 | - cab dir=U
4 | -
5 | -
6 | - -
7 | - eng dir=L;pow=150;keys=30
8 | - box
9 | - eng dir=R;pow=150;keys=29
10 | -
11 | - - box
12 | - box
13 | - crn dir=D
14 | - box
15 | - box
16 | - - eng dir=D;pow=150;keys=30,31
17 | - eng dir=D;pow=150;keys=30,31
18 | -
19 | - eng dir=D;pow=150;keys=29,31
20 | - eng dir=D;pow=150;keys=29,31
21 |
22 |
23 | energy: 100
24 | fuel: 5000
25 | maxfuel: 5000
26 |
--------------------------------------------------------------------------------
/localization.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | const (
4 | VisitPlatformText = "Visit platform %v"
5 | TransferCargoText = "Transfer cargo %v to platform %v"
6 | DistanceText = "%vm"
7 | FuelLabelText = "FUEL"
8 | EnergyLabelText = "ENERGY"
9 | TargetLabelText = "TARGET"
10 | DistanceLabelText = "DISTANCE"
11 |
12 | LevelCompleteText = "Level Complete"
13 | LevelFailedText = "Level Failed"
14 | LevelExitText = "Exit Level"
15 | PressEnterToContinue = "Press ENTER to continue"
16 | )
17 |
--------------------------------------------------------------------------------
/ebiten_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hajimehoshi/ebiten/v2"
5 | "image/color"
6 | "testing"
7 | )
8 |
9 | func BenchmarkEbitenNewImage(b *testing.B) {
10 | for i := 0; i < b.N; i++ {
11 | ebiten.NewImage(500, 30)
12 | }
13 | }
14 |
15 | func BenchmarkClearNewImage(b *testing.B) {
16 | img := ebiten.NewImage(500, 30)
17 | for i := 0; i < b.N; i++ {
18 | img.Clear()
19 | }
20 | }
21 |
22 | func BenchmarkFillNewImage(b *testing.B) {
23 | img := ebiten.NewImage(500, 30)
24 | for i := 0; i < b.N; i++ {
25 | img.Fill(color.White)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/svg/parser.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "encoding/xml"
5 | "io"
6 | "io/ioutil"
7 | "os"
8 | )
9 |
10 | func Load(fileName string) (Svg, error) {
11 | file, err := os.Open(fileName)
12 | if err != nil {
13 | return Svg{}, err
14 | }
15 | defer file.Close()
16 | return Parse(file)
17 | }
18 |
19 | func Parse(file io.Reader) (Svg, error) {
20 | data, err := ioutil.ReadAll(file)
21 | if err != nil {
22 | return Svg{}, err
23 | }
24 |
25 | var result Svg
26 | if err := xml.Unmarshal(data, &result); err != nil {
27 | return Svg{}, err
28 | }
29 |
30 | return result, nil
31 | }
32 |
--------------------------------------------------------------------------------
/consts.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | const (
4 | DrawDebugBodies = false
5 | PrintDebugInfo = true
6 |
7 | ScreenWidth = 1920
8 | ScreenHeight = 1080
9 | FontDpi = 72
10 |
11 | Gravity = 1
12 | DefaultFixtureDensity = 20
13 | DefaultFixtureRestitution = 0.2
14 | DefaultFriction = 1
15 | EngineFuelConsumption = 0.01
16 | AssetsDir = "assets"
17 | ShipsDir = "assets/ships"
18 | PartsDir = "parts"
19 | TerrainsDir = "terrains"
20 | LevelsDir = "levels"
21 |
22 | MinCamZoom, MaxCamZoom = 20.0, 80.0
23 | )
24 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | on:
2 | release:
3 | types: [created]
4 |
5 | jobs:
6 | release-linux-amd64:
7 | name: release linux/amd64
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64
12 | goos: [ windows ]
13 | goarch: [ amd64 ]
14 | exclude:
15 | - goarch: "386"
16 | goos: darwin
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: wangyoucao577/go-release-action@v1.20
20 | with:
21 | github_token: ${{ secrets.GITHUB_TOKEN }}
22 | goos: ${{ matrix.goos }}
23 | goarch: ${{ matrix.goarch }}
24 | binary_name: "space-crane"
25 | extra_files: assets levels
--------------------------------------------------------------------------------
/cam.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/ByteArena/box2d"
4 |
5 | type Cam struct {
6 | Pos box2d.B2Vec2
7 | Zoom float64
8 | }
9 |
10 | func NewCam() *Cam {
11 | return &Cam{
12 | Pos: box2d.MakeB2Vec2(50, 20),
13 | Zoom: 20,
14 | }
15 | }
16 |
17 | func (c *Cam) Project(v box2d.B2Vec2, pos box2d.B2Vec2, ang float64) box2d.B2Vec2 {
18 | v = box2d.B2RotVec2Mul(*box2d.NewB2RotFromAngle(ang), v) // rotate
19 | v = box2d.B2Vec2Add(v, pos) // set position
20 | v = box2d.B2Vec2Add(v, box2d.MakeB2Vec2(-c.Pos.X, -c.Pos.Y)) // camera pos
21 | v = box2d.B2Vec2MulScalar(c.Zoom, v) // camera zoom
22 | v = box2d.B2Vec2Add(v, box2d.MakeB2Vec2(ScreenWidth/2, ScreenHeight/2)) // cam screen center
23 | return v
24 | }
25 |
--------------------------------------------------------------------------------
/background.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/hajimehoshi/ebiten/v2"
4 |
5 | type Background struct {
6 | img *ebiten.Image
7 | }
8 |
9 | func NewBackground() Background {
10 | return Background{
11 | img: loadImage("paper.jpg"),
12 | }
13 | }
14 |
15 | func (b *Background) Draw(screen *ebiten.Image, cam Cam) {
16 | bounds := b.img.Bounds()
17 |
18 | opts := &ebiten.DrawImageOptions{}
19 | screen.DrawImage(b.img, opts)
20 |
21 | opts = &ebiten.DrawImageOptions{}
22 | opts.GeoM.Translate(float64(bounds.Max.X), 0)
23 | screen.DrawImage(b.img, opts)
24 |
25 | opts = &ebiten.DrawImageOptions{}
26 | opts.GeoM.Translate(0, float64(bounds.Max.Y))
27 | screen.DrawImage(b.img, opts)
28 |
29 | opts = &ebiten.DrawImageOptions{}
30 | opts.GeoM.Translate(float64(bounds.Max.X), float64(bounds.Max.Y))
31 | screen.DrawImage(b.img, opts)
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module go-space-crane
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/ByteArena/box2d v1.0.2
7 | github.com/hajimehoshi/ebiten/v2 v2.2.0
8 | github.com/stretchr/testify v1.7.0
9 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
11 | )
12 |
13 | require (
14 | github.com/davecgh/go-spew v1.1.0 // indirect
15 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
16 | github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
17 | github.com/pmezard/go-difflib v1.0.0 // indirect
18 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
19 | golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 // indirect
20 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
21 | golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
22 | golang.org/x/text v0.3.6 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/visit_platform_task.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ByteArena/box2d"
6 | )
7 |
8 | type VisitPlatformTask struct {
9 | Platform *Platform
10 | isComplete bool
11 | }
12 |
13 | func NewVisitPlatformTask(platform *Platform) *VisitPlatformTask {
14 | return &VisitPlatformTask{Platform: platform}
15 | }
16 |
17 | func (t *VisitPlatformTask) Pos() box2d.B2Vec2 {
18 | return t.Platform.GetPos()
19 | }
20 |
21 | func (t *VisitPlatformTask) TargetName() string {
22 | return fmt.Sprintf(VisitPlatformText, t.Platform.id)
23 | }
24 |
25 | func (t *VisitPlatformTask) IsComplete() bool {
26 | return t.isComplete
27 | }
28 |
29 | func (t *VisitPlatformTask) ShipLanded(p *Platform) {
30 | if t.IsComplete() {
31 | return
32 | }
33 | if p.id == t.Platform.id {
34 | t.isComplete = true
35 | }
36 | }
37 |
38 | func (t *VisitPlatformTask) CargoOnPlatform(c *Cargo, p *Platform) {
39 | }
40 |
--------------------------------------------------------------------------------
/deliver_cargo_task.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ByteArena/box2d"
6 | )
7 |
8 | type DeliverCargoTask struct {
9 | Cargo *Cargo
10 | Platform *Platform
11 | isComplete bool
12 | }
13 |
14 | func NewDeliverCargoTask(cargo *Cargo, platform *Platform) *DeliverCargoTask {
15 | return &DeliverCargoTask{
16 | Cargo: cargo,
17 | Platform: platform,
18 | }
19 | }
20 |
21 | func (t *DeliverCargoTask) Pos() box2d.B2Vec2 {
22 | // TODO: is cargo not captured, show its position
23 | return t.Platform.GetPos()
24 | }
25 |
26 | func (t *DeliverCargoTask) TargetName() string {
27 | return fmt.Sprintf(TransferCargoText, t.Cargo.id, t.Platform.id)
28 | }
29 |
30 | func (t *DeliverCargoTask) IsComplete() bool {
31 | return t.isComplete
32 | }
33 |
34 | func (t *DeliverCargoTask) ShipLanded(p *Platform) {
35 | }
36 |
37 | func (t *DeliverCargoTask) CargoOnPlatform(c *Cargo, p *Platform) {
38 | if c.id == t.Cargo.id && p.id == t.Platform.id {
39 | t.isComplete = true
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/svg/rect.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "encoding/xml"
5 | "github.com/ByteArena/box2d"
6 | )
7 |
8 | type Rect struct {
9 | ID string
10 | Title string
11 | Description string
12 | Pos box2d.B2Vec2
13 | Size box2d.B2Vec2
14 | }
15 |
16 | func (r *Rect) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
17 | var prj struct {
18 | ID string `xml:"id,attr"`
19 | Title string `xml:"title"`
20 | Description string `xml:"desc"`
21 | X float64 `xml:"x,attr"`
22 | Y float64 `xml:"y,attr"`
23 | Width float64 `xml:"width,attr"`
24 | Height float64 `xml:"height,attr"`
25 | }
26 |
27 | if err := d.DecodeElement(&prj, &start); err != nil {
28 | return err
29 | }
30 |
31 | *r = Rect{
32 | ID: prj.ID,
33 | Title: prj.Title,
34 | Description: prj.Description,
35 | Pos: box2d.MakeB2Vec2(prj.X, prj.Y),
36 | Size: box2d.MakeB2Vec2(prj.Width, prj.Height),
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/svg/ellipse.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "encoding/xml"
5 | "github.com/ByteArena/box2d"
6 | )
7 |
8 | type Ellipse struct {
9 | // TODO: id, title, desc to struct
10 | ID string
11 | Title string
12 | Description string
13 |
14 | Pos box2d.B2Vec2
15 | Radius box2d.B2Vec2
16 | }
17 |
18 | func (e *Ellipse) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
19 | var prj struct {
20 | ID string `xml:"id,attr"`
21 | Title string `xml:"title"`
22 | Description string `xml:"desc"`
23 | Cx float64 `xml:"cx,attr"`
24 | Cy float64 `xml:"cy,attr"`
25 | Rx float64 `xml:"rx,attr"`
26 | Ry float64 `xml:"ry,attr"`
27 | }
28 |
29 | if err := d.DecodeElement(&prj, &start); err != nil {
30 | return err
31 | }
32 |
33 | *e = Ellipse{
34 | ID: prj.ID,
35 | Title: prj.Title,
36 | Description: prj.Description,
37 | Pos: box2d.MakeB2Vec2(prj.Cx, prj.Cy),
38 | Radius: box2d.MakeB2Vec2(prj.Rx, prj.Ry),
39 | }
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: 1.17
20 |
21 |
22 | - name: Update
23 | run: sudo apt-get update
24 |
25 | - name: Install deps
26 | run: sudo apt install libc6-dev libglu1-mesa-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libasound2-dev pkg-config
27 |
28 | - name: Xvfb
29 | run: |
30 | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
31 |
32 | - name: Build
33 | run: go build -v ./...
34 |
35 | - name: Test
36 | env:
37 | DISPLAY: ':99.0'
38 | run: go test ./... -coverprofile coverage.txt
39 |
40 | - name: Upload Coverage report to CodeCov
41 | uses: codecov/codecov-action@v1.0.0
42 | with:
43 | token: ${{secrets.CODECOV_TOKEN}}
44 | file: ./coverage.txt
45 |
--------------------------------------------------------------------------------
/modal.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hajimehoshi/ebiten/v2"
5 | "github.com/hajimehoshi/ebiten/v2/text"
6 | "image/color"
7 | )
8 |
9 | type Modal struct {
10 | Title string
11 | Text string
12 | CloseKeys Keys
13 | isClosed bool
14 | }
15 |
16 | func NewModal(title string, text string, closeKeys Keys) *Modal {
17 | return &Modal{
18 | Title: title,
19 | Text: text,
20 | CloseKeys: closeKeys,
21 | }
22 | }
23 |
24 | func (m *Modal) Update(keys Keys) {
25 | found := false
26 | for key := range m.CloseKeys {
27 | if keys.IsPressed(key) {
28 | found = true
29 | break
30 | }
31 | }
32 | if found {
33 | m.isClosed = true
34 | }
35 | }
36 |
37 | func (m *Modal) Draw(screen *ebiten.Image) {
38 | if m.isClosed {
39 | return
40 | }
41 |
42 | screen.DrawImage(modalImg, nil)
43 |
44 | titleBounds := text.BoundString(modalTitleFace, m.Title)
45 | text.Draw(screen, m.Title, modalTitleFace, ScreenWidth/2-titleBounds.Max.X/2, 280, color.White)
46 |
47 | textBounds := text.BoundString(modalTextFace, m.Text)
48 | text.Draw(screen, m.Text, modalTextFace, ScreenWidth/2-textBounds.Max.X/2, 500, color.White)
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/task.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ByteArena/box2d"
6 | "strings"
7 | )
8 |
9 | type Task interface {
10 | // Pos is the position of task object
11 | TargetName() string
12 | Pos() box2d.B2Vec2
13 | IsComplete() bool
14 | ShipLanded(p *Platform)
15 | CargoOnPlatform(c *Cargo, p *Platform)
16 | }
17 |
18 | func ParseTaskDef(def string, platforms map[string]*Platform, cargos map[string]*Cargo) Task {
19 | taskType := def[0:1]
20 | switch taskType {
21 | case "v":
22 | pid := def[2:]
23 | p, found := platforms[pid]
24 | if !found {
25 | checkErr(fmt.Errorf("%v platform not found", pid))
26 | }
27 | return NewVisitPlatformTask(p)
28 | case "d":
29 | parts := strings.Split(def[2:], "->")
30 | if len(parts) != 2 {
31 | checkErr(fmt.Errorf("bad deliver cargo task defenition: %v", def[2:]))
32 | }
33 | cid := parts[0]
34 | pid := parts[1]
35 | p, found := platforms[pid]
36 | if !found {
37 | checkErr(fmt.Errorf("%v platform not found", pid))
38 | }
39 | c, found := cargos[cid]
40 | if !found {
41 | checkErr(fmt.Errorf("%v cargo not found", cid))
42 | }
43 | return NewDeliverCargoTask(c, p)
44 | default:
45 | checkErr(fmt.Errorf("unknown task type %v", taskType))
46 | return nil
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/direction.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "math"
6 | )
7 |
8 | type Direction string
9 |
10 | const (
11 | DirectionDown Direction = "D"
12 | DirectionLeft Direction = "L"
13 | DirectionUp Direction = "U"
14 | DirectionRight Direction = "R"
15 | )
16 |
17 | func (d Direction) GetAng() float64 {
18 | switch d {
19 | case DirectionRight:
20 | return 0
21 | case DirectionDown:
22 | return math.Pi / 2
23 | case DirectionLeft:
24 | return math.Pi
25 | case DirectionUp:
26 | return math.Pi + math.Pi/2
27 | default:
28 | panic("bad direction: " + d)
29 | }
30 | }
31 |
32 | func (d Direction) GetVec() box2d.B2Vec2 {
33 | switch d {
34 | case DirectionRight:
35 | return box2d.MakeB2Vec2(1, 0)
36 | case DirectionDown:
37 | return box2d.MakeB2Vec2(0, 1)
38 | case DirectionLeft:
39 | return box2d.MakeB2Vec2(-1, 0)
40 | case DirectionUp:
41 | return box2d.MakeB2Vec2(0, -1)
42 | }
43 | panic("bad direction")
44 | }
45 |
46 | func (d Direction) Negative() Direction {
47 | switch d {
48 | case DirectionRight:
49 | return DirectionLeft
50 | case DirectionDown:
51 | return DirectionUp
52 | case DirectionLeft:
53 | return DirectionRight
54 | case DirectionUp:
55 | return DirectionDown
56 | }
57 | panic("bad direction")
58 | }
59 |
--------------------------------------------------------------------------------
/game_decorator.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | )
7 |
8 | // TODO: description
9 | // TODO: rename
10 | // We need it because ebiten allows to run only one game
11 | type GameDecorator struct {
12 | g ebiten.Game
13 |
14 | cam *Cam
15 | ps *ParticleSystem
16 | bg Background
17 | }
18 |
19 | func NewGameDecorator() *GameDecorator {
20 | cam := NewCam()
21 | ps := NewParticleSystem()
22 | bg := NewBackground()
23 |
24 | return &GameDecorator{
25 | g: NewLevelSelector(),
26 |
27 | cam: cam,
28 | ps: ps,
29 | bg: bg,
30 | }
31 | }
32 |
33 | func (gd *GameDecorator) Update() error {
34 | err := gd.g.Update()
35 | if selectedLevel, ok := err.(*SelectedLevel); ok {
36 | world := box2d.MakeB2World(box2d.MakeB2Vec2(0, Gravity))
37 | level := LoadLevel(&world, gd.ps, selectedLevel.name)
38 | game := NewGame(&world, gd.cam, gd.bg, gd.ps, level)
39 | world.SetContactListener(game)
40 | gd.g = game
41 | }
42 |
43 | switch err {
44 | case levelComplete, levelFailed:
45 | gd.g = NewLevelSelector()
46 | }
47 | return nil
48 | }
49 |
50 | func (gd *GameDecorator) Draw(screen *ebiten.Image) {
51 | gd.g.Draw(screen)
52 | }
53 |
54 | func (gd *GameDecorator) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
55 | return ScreenWidth, ScreenHeight
56 | }
57 |
--------------------------------------------------------------------------------
/cargo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/text"
7 | "image/color"
8 | )
9 |
10 | type Cargo struct {
11 | *GameObj
12 | id string
13 | size box2d.B2Vec2
14 | platform *Platform
15 | tasks []Task
16 | }
17 |
18 | func NewCargo(id string, world *box2d.B2World, pos box2d.B2Vec2) *Cargo {
19 | // Replace cargo image with new one with ID
20 | img := ebiten.NewImage(cargoSprite.img.Size())
21 | img.DrawImage(cargoSprite.img, nil)
22 | textBounds := text.BoundString(cargoFace, id)
23 | cargoSize := box2d.MakeB2Vec2(300, 300)
24 | text.Draw(img, id, cargoFace, int(cargoSize.X/2)-textBounds.Max.X/2, int(cargoSize.Y/2)-textBounds.Min.Y/2, color.White)
25 | sprite := NewSprite(img, cargoSprite.vertsSet)
26 |
27 | gameObj := NewGameObj(
28 | world,
29 | sprite,
30 | pos,
31 | 0,
32 | 0,
33 | box2d.B2Vec2_zero,
34 | DefaultFriction,
35 | DefaultFixtureDensity,
36 | DefaultFixtureRestitution, true)
37 |
38 | cargo := &Cargo{
39 | GameObj: gameObj,
40 | id: id,
41 | size: getShapeSize(cargoSprite.vertsSet[0]),
42 | }
43 | cargo.body.SetUserData(cargo)
44 | return cargo
45 | }
46 |
47 | func (c *Cargo) Update() {
48 | if c.platform != nil && FloatEquals(c.GetVel(), 0) {
49 | for _, task := range c.tasks {
50 | task.CargoOnPlatform(c, c.platform)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/assets/parts/chain_el.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
46 |
--------------------------------------------------------------------------------
/assets/parts/cargo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
47 |
--------------------------------------------------------------------------------
/sprite.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | svg2 "go-space-crane/svg"
7 | "path"
8 | )
9 |
10 | // Sprite contains sprite image and shape vertices
11 | type Sprite struct {
12 | img *ebiten.Image
13 | vertsSet [][]box2d.B2Vec2
14 | }
15 |
16 | // TODO: move from sprite
17 | // TODO: rename to ScaleEngine
18 | func (s Sprite) Scale(f float64, dir Direction) Sprite {
19 | if f == 1 {
20 | return s
21 | }
22 |
23 | vertsSet := make([][]box2d.B2Vec2, len(s.vertsSet))
24 | for y, row := range s.vertsSet {
25 | vertsSet[y] = make([]box2d.B2Vec2, len(row))
26 | for x, vec := range row {
27 | vertsSet[y][x] = box2d.B2Vec2MulScalar(f, vec)
28 | vertsSet[y][x].X -= 0.5 - f/2
29 | }
30 | }
31 |
32 | bounds := s.img.Bounds()
33 | img := ebiten.NewImage(bounds.Max.X, bounds.Max.Y)
34 | opts := &ebiten.DrawImageOptions{}
35 | opts.GeoM.Scale(f, f)
36 | opts.GeoM.Translate(
37 | 0,
38 | float64(bounds.Max.Y)/2-float64(bounds.Max.Y)*f/2)
39 |
40 | img.DrawImage(s.img, opts)
41 |
42 | return NewSprite(img, vertsSet)
43 | }
44 |
45 | func NewSprite(img *ebiten.Image, vertsSet [][]box2d.B2Vec2) Sprite {
46 | return Sprite{img: img, vertsSet: vertsSet}
47 | }
48 |
49 | func LoadPart(name string) Sprite {
50 | name = path.Join(PartsDir, name)
51 | img := loadImage(name + ".png")
52 | svg, err := svg2.Load(path.Join(AssetsDir, name+".svg"))
53 | checkErr(err)
54 |
55 | vSet := make([][]box2d.B2Vec2, len(svg.Layers[0].Pathes))
56 | for i, path := range svg.Layers[0].Pathes {
57 | vSet[i] = path.Verts
58 | }
59 |
60 | return Sprite{
61 | img: img,
62 | vertsSet: vSet,
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/assets/parts/crane.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/assets/parts/leg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/assets/parts/engine.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/assets/parts/leg_fastening.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/assets/parts/cabin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/terrain.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/vector"
7 | )
8 |
9 | type Terrain struct {
10 | body *box2d.B2Body
11 | vSet [][]box2d.B2Vec2
12 | }
13 |
14 | func NewTerrain(world *box2d.B2World, vSet [][]box2d.B2Vec2) *Terrain {
15 | bd := box2d.MakeB2BodyDef()
16 | bd.Position.Set(0, 0)
17 | bd.Type = box2d.B2BodyType.B2_staticBody
18 | body := world.CreateBody(&bd)
19 |
20 | for _, verts := range vSet {
21 | shape := box2d.MakeB2ChainShape()
22 | shape.CreateLoop(verts, len(verts))
23 |
24 | fd := box2d.MakeB2FixtureDef()
25 | fd.Filter = box2d.MakeB2Filter()
26 | fd.Shape = &shape
27 | fd.Friction = DefaultFriction
28 | fd.Density = DefaultFixtureDensity
29 | fd.Restitution = DefaultFixtureRestitution
30 | body.CreateFixtureFromDef(&fd)
31 | }
32 |
33 | return &Terrain{
34 | body: body,
35 | vSet: vSet,
36 | }
37 | }
38 |
39 | func (g *Terrain) Draw(screen *ebiten.Image, cam Cam) {
40 | for _, vecs := range g.vSet {
41 | var path vector.Path
42 |
43 | v := cam.Project(vecs[0], box2d.B2Vec2_zero, 0)
44 | path.MoveTo(float32(v.X), float32(v.Y))
45 | for i := 1; i < len(vecs); i++ {
46 | v := cam.Project(vecs[i], box2d.B2Vec2_zero, 0)
47 | path.LineTo(float32(v.X), float32(v.Y))
48 | }
49 |
50 | opts := &ebiten.DrawTrianglesOptions{
51 | FillRule: ebiten.EvenOdd,
52 | }
53 |
54 | vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil)
55 | for i := range vs {
56 | vs[i].SrcX = 1
57 | vs[i].SrcY = 1
58 | vs[i].ColorR = 0.2
59 | vs[i].ColorG = 0.2
60 | vs[i].ColorB = 0.2
61 | }
62 | screen.DrawTriangles(vs, is, emptySubImage, opts)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/assets/parts/box.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Space Crane game
2 | ================
3 |
4 |
5 | 
6 | 
7 | [](https://codecov.io/gh/spiritofsim/go-space-crane)
8 | [](https://goreportcard.com/report/github.com/spiritofsim/go-space-crane)
9 |
10 | ## Controls
11 | - **Arrows** - control ship engines
12 | - **Q**/**A** - wind/unwind crane chain
13 | - **Tab** - capture cargo
14 |
15 |
16 | ## Screenshots
17 | 
18 |
19 | ## Features
20 | ### Levels
21 | - Levels are simple svg's you can draw in any SVG editor
22 | 
23 |
24 |
25 | ### Ships
26 |
27 | Ship consists of parts and described in simple yaml
28 |
29 | **Parts**
30 | - Cabin [**cab**]
31 | - Engine [**eng**]
32 | - Crane [**crn**]
33 | - Tank [**tnk**]
34 | - Leg [**leg**]
35 | - LegFastening [**lft**]
36 |
37 | **Example**
38 | ```yaml
39 | - -
40 | - cab dir=U
41 | -
42 | - - tnk
43 | - crn dir=D
44 | - tnk
45 | - - eng dir=D;pow=150;keys=30,31
46 | -
47 | - eng dir=D;pow=150;keys=29,31
48 | ```
49 |
50 | ### Tasks
51 |
52 | There are 2 task types:
53 | - [**v**] - visit platform task
54 | - [**d**] - transfer cargo to platform
55 |
56 | **Example**
57 | ```yaml
58 | tasks:
59 | - "d:1->p1"
60 | - "d:1->p2"
61 | - "v:p1"
62 | ```
63 |
64 | ## Libs
65 | - [Box2D](https://github.com/ByteArena/box2d) : physics engine
66 | - [Ebiten](https://ebiten.org/) : drawings, controls, etc.
--------------------------------------------------------------------------------
/assets/parts/crane_lower_jaw.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/assets/parts/crane_upper_jaw.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/ebitenutil"
7 | _ "image/jpeg"
8 | _ "image/png"
9 | "math"
10 | "math/rand"
11 | "path"
12 | )
13 |
14 | func RandFloat(min, max float64) float64 {
15 | return min + rand.Float64()*(max-min)
16 | }
17 |
18 | func RandInt(min, max int) int {
19 | return rand.Intn(max-min) + min
20 | }
21 |
22 | const FloatTolerance = 0.05
23 |
24 | func FloatEquals(a, b float64) bool {
25 | delta := math.Abs(a - b)
26 | return delta < FloatTolerance
27 | }
28 |
29 | func Remap(val, from1, to1, from2, to2 float64) float64 {
30 | return (val-from1)/(to1-from1)*(to2-from2) + from2
31 | }
32 |
33 | func loadImage(name string) *ebiten.Image {
34 | img, _, err := ebitenutil.NewImageFromFile(path.Join(AssetsDir, name))
35 | checkErr(err)
36 | return img
37 | }
38 |
39 | func getShapeSize(verts []box2d.B2Vec2) box2d.B2Vec2 {
40 | min, max := verts[0], verts[0]
41 | for i := 1; i < len(verts); i++ {
42 | if verts[i].X < min.X {
43 | min.X = verts[i].X
44 | }
45 | if verts[i].Y < min.Y {
46 | min.Y = verts[i].Y
47 | }
48 | if verts[i].X > max.X {
49 | max.X = verts[i].X
50 | }
51 | if verts[i].Y > max.Y {
52 | max.Y = verts[i].Y
53 | }
54 | }
55 |
56 | return box2d.MakeB2Vec2(max.X-min.X, max.Y-min.Y)
57 | }
58 |
59 | func GetVecsAng(a, b box2d.B2Vec2) (ang float64, dist float64) {
60 | x := box2d.B2Vec2Add(b, a.OperatorNegate())
61 | dist = x.Length()
62 | x.Normalize()
63 | rot := box2d.B2Rot{
64 | S: x.Y,
65 | C: x.X,
66 | }
67 | ang = rot.GetAngle()
68 | return
69 | }
70 |
71 | type Keys map[ebiten.Key]struct{}
72 |
73 | func (keys Keys) IsPressed(key ebiten.Key) bool {
74 | _, ok := keys[key]
75 | return ok
76 | }
77 |
78 | func KeysFromSlice(keys []ebiten.Key) Keys {
79 | result := make(Keys)
80 | for _, key := range keys {
81 | result[key] = struct{}{}
82 | }
83 | return result
84 | }
85 |
--------------------------------------------------------------------------------
/game_obj.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "math"
7 | )
8 |
9 | type GameObj struct {
10 | world *box2d.B2World
11 | body *box2d.B2Body
12 | img *ebiten.Image
13 | }
14 |
15 | func NewGameObj(
16 | world *box2d.B2World,
17 | sprite Sprite,
18 | pos box2d.B2Vec2,
19 | ang float64,
20 | aVel float64,
21 | lVel box2d.B2Vec2,
22 | friction float64,
23 | density float64,
24 | restitution float64,
25 | isDynamic bool) *GameObj {
26 |
27 | bd := box2d.MakeB2BodyDef()
28 | bd.Position.Set(pos.X, pos.Y)
29 | bd.Angle = ang
30 | if isDynamic {
31 | bd.Type = box2d.B2BodyType.B2_dynamicBody
32 | } else {
33 | bd.Type = box2d.B2BodyType.B2_staticBody
34 | }
35 | body := world.CreateBody(&bd)
36 |
37 | for _, verts := range sprite.vertsSet {
38 | shape := box2d.MakeB2PolygonShape()
39 | shape.Set(verts, len(verts))
40 |
41 | fd := box2d.MakeB2FixtureDef()
42 | fd.Filter = box2d.MakeB2Filter()
43 | fd.Shape = &shape
44 | fd.Friction = friction
45 | fd.Density = density
46 | fd.Restitution = restitution
47 | body.CreateFixtureFromDef(&fd)
48 | }
49 |
50 | body.SetAngularVelocity(aVel)
51 | body.SetLinearVelocity(lVel)
52 |
53 | return &GameObj{
54 | world: world,
55 | body: body,
56 | img: sprite.img,
57 | }
58 | }
59 |
60 | func (g *GameObj) GetVel() float64 {
61 | lVel := g.body.GetLinearVelocity()
62 | return math.Sqrt(lVel.X*lVel.X + lVel.Y*lVel.Y)
63 | }
64 |
65 | func (g *GameObj) GetVelVec() box2d.B2Vec2 {
66 | return g.body.GetLinearVelocity()
67 | }
68 |
69 | func (g *GameObj) GetAng() float64 {
70 | return g.body.GetAngle()
71 | }
72 |
73 | func (g *GameObj) GetPos() box2d.B2Vec2 {
74 | return g.body.GetPosition()
75 | }
76 |
77 | // TODO: move this to sprite
78 | func (g *GameObj) Draw(screen *ebiten.Image, cam Cam) {
79 | opts := &ebiten.DrawImageOptions{}
80 | bounds := g.img.Bounds()
81 | pos := g.body.GetPosition()
82 | opts.GeoM.Translate(-float64(bounds.Max.X/2), -float64(bounds.Max.Y/2))
83 | opts.GeoM.Scale(1/float64(bounds.Max.X), 1/float64(bounds.Max.Y))
84 | opts.GeoM.Rotate(g.body.GetAngle())
85 | opts.GeoM.Translate(pos.X, pos.Y)
86 | opts.GeoM.Translate(-cam.Pos.X, -cam.Pos.Y)
87 | opts.GeoM.Scale(cam.Zoom, cam.Zoom)
88 | opts.GeoM.Translate(ScreenWidth/2, ScreenHeight/2)
89 |
90 | screen.DrawImage(g.img, opts)
91 | }
92 |
--------------------------------------------------------------------------------
/engine.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "math"
7 | )
8 |
9 | type EngineDef struct {
10 | Dir Direction
11 | Power float64
12 | Keys Keys
13 | Size float64
14 | }
15 |
16 | type Engine struct {
17 | *GameObj
18 | tanker Tank
19 | ps *ParticleSystem
20 | power float64
21 | keys Keys
22 | isActive bool
23 | }
24 |
25 | func (d EngineDef) Construct(
26 | world *box2d.B2World,
27 | tanker Tank,
28 | ps *ParticleSystem,
29 | shipPos box2d.B2Vec2,
30 | shipSize box2d.B2Vec2,
31 | pos box2d.B2Vec2) Part {
32 |
33 | // TODO: duplicate in basic_part
34 | shipHalfSize := box2d.B2Vec2MulScalar(0.5, shipSize)
35 | worldPos := box2d.B2Vec2Add(shipPos, pos)
36 | worldPos = box2d.B2Vec2Add(worldPos, shipHalfSize.OperatorNegate())
37 | worldPos = box2d.B2Vec2Add(worldPos, box2d.MakeB2Vec2(0.5, 0.5))
38 |
39 | sprite := engineSprite.Scale(d.Size, d.Dir)
40 | gameObj := NewGameObj(
41 | world,
42 | sprite,
43 | worldPos,
44 | d.Dir.GetAng(),
45 | 0,
46 | box2d.B2Vec2_zero,
47 | DefaultFriction, DefaultFixtureDensity, DefaultFixtureRestitution, true)
48 |
49 | engine := &Engine{
50 | GameObj: gameObj,
51 | tanker: tanker,
52 | power: d.Power,
53 | ps: ps,
54 | keys: d.Keys,
55 | }
56 | engine.GetBody().SetUserData(engine)
57 |
58 | return engine
59 | }
60 |
61 | func (e *Engine) Draw(screen *ebiten.Image, cam Cam) {
62 | e.GameObj.Draw(screen, cam)
63 |
64 | if !e.isActive {
65 | return
66 | }
67 |
68 | // TODO: fix emit for small engines
69 | // Flame particles
70 | pos := box2d.B2Vec2Add(
71 | e.GetPos(),
72 | box2d.B2RotVec2Mul(*box2d.NewB2RotFromAngle(e.GetAng()), box2d.MakeB2Vec2(0.5, 0)))
73 | e.ps.
74 | Emit(pos, e.GetAng(), math.Pi/4)
75 | }
76 |
77 | func (e *Engine) GetBody() *box2d.B2Body {
78 | return e.body
79 | }
80 |
81 | func (e *Engine) Update(keys Keys) {
82 | e.isActive = false
83 | if e.tanker.GetFuel() <= 0 {
84 | return
85 | }
86 |
87 | // TODO: to func
88 | keyFound := false
89 | for key := range keys {
90 | if e.keys.IsPressed(key) {
91 | keyFound = true
92 | break
93 | }
94 | }
95 | if !keyFound {
96 | return
97 | }
98 | e.isActive = true
99 |
100 | rot := box2d.NewB2RotFromAngle(e.GetAng())
101 | force := box2d.B2RotVec2Mul(*rot, box2d.MakeB2Vec2(e.power, 0))
102 | force = force.OperatorNegate()
103 | e.body.ApplyForce(force, e.body.GetPosition(), true)
104 |
105 | e.tanker.ReduceFuel(e.power * EngineFuelConsumption)
106 | }
107 |
--------------------------------------------------------------------------------
/part_def.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ByteArena/box2d"
6 | "github.com/hajimehoshi/ebiten/v2"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | type PartType string
12 |
13 | const (
14 | PartTypeBox PartType = "box"
15 | PartTypeCabin PartType = "cab"
16 | PartTypeEngine PartType = "eng"
17 | PartTypeCrane PartType = "crn"
18 | PartTypeLeg PartType = "leg"
19 | PartTypeLegFastening PartType = "lft"
20 | )
21 |
22 | type PartParam string
23 |
24 | const (
25 | PartParamDir PartParam = "dir"
26 | PartParamPower PartParam = "pow"
27 | PartParamSize PartParam = "size"
28 | PartParamKeys PartParam = "keys"
29 | )
30 |
31 | type PartDef interface {
32 | Construct(world *box2d.B2World,
33 | tanker Tank,
34 | ps *ParticleSystem,
35 | shipPos box2d.B2Vec2,
36 | shipSize box2d.B2Vec2,
37 | pos box2d.B2Vec2) Part
38 | }
39 |
40 | func ParsePartDef(strP *string) PartDef {
41 | if strP == nil {
42 | return nil
43 | }
44 | str := *strP
45 | tp := strings.ToLower(str[:3])
46 | params := parsePartParams(str[3:])
47 | switch PartType(tp) {
48 | case PartTypeBox:
49 | return &BoxDef{}
50 | case PartTypeCabin:
51 | return &CabinDef{
52 | Dir: params[PartParamDir].AsDirection(),
53 | }
54 | case PartTypeEngine:
55 | size := 1.0
56 | if s, ok := params[PartParamSize]; ok {
57 | size = s.AsFloat()
58 | }
59 | return &EngineDef{
60 | Dir: params[PartParamDir].AsDirection(),
61 | Power: params[PartParamPower].AsFloat(),
62 | Keys: params[PartParamKeys].AsKeys(),
63 | Size: size,
64 | }
65 | case PartTypeCrane:
66 | return &CraneDef{
67 | Dir: params[PartParamDir].AsDirection(),
68 | }
69 | case PartTypeLeg:
70 | return &LegDef{
71 | Dir: params[PartParamDir].AsDirection(),
72 | }
73 | case PartTypeLegFastening:
74 | return &LegFasteningDef{
75 | Dir: params[PartParamDir].AsDirection(),
76 | }
77 | default:
78 | checkErr(fmt.Errorf("unknown part type %v", tp))
79 | return nil
80 | }
81 | }
82 |
83 | type paramVal string
84 |
85 | func (v paramVal) AsDirection() Direction {
86 | return Direction(v)
87 | }
88 | func (v paramVal) AsFloat() float64 {
89 | result, err := strconv.ParseFloat(string(v), 64)
90 | checkErr(err)
91 | return result
92 | }
93 | func (v paramVal) AsKeys() Keys {
94 | result := make(Keys)
95 | for _, elem := range strings.Split(string(v), ",") {
96 | k, err := strconv.Atoi(elem)
97 | checkErr(err)
98 | result[ebiten.Key(k)] = struct{}{}
99 | }
100 | return result
101 | }
102 |
103 | func parsePartParams(str string) map[PartParam]paramVal {
104 | result := make(map[PartParam]paramVal)
105 | params := strings.Split(str, ";")
106 | if len(params) == 1 && params[0] == "" {
107 | return result
108 | }
109 | for _, param := range params {
110 | kv := strings.Split(param, "=")
111 | if len(kv) != 2 {
112 | checkErr(fmt.Errorf("bad part param %v", str))
113 | }
114 | result[PartParam(strings.Trim(kv[0], " "))] = paramVal(strings.Trim(kv[1], " "))
115 | }
116 | return result
117 | }
118 |
--------------------------------------------------------------------------------
/ps.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "math"
7 | )
8 |
9 | type ColorScale struct {
10 | R, G, B, A float64
11 | }
12 |
13 | type Particle struct {
14 | img *ebiten.Image
15 | crl ColorScale
16 |
17 | pos box2d.B2Vec2
18 | lvel box2d.B2Vec2
19 | ang float64
20 | avel float64
21 |
22 | age int
23 | ttl int
24 | }
25 |
26 | func NewParticle(
27 | ttl int,
28 | pos box2d.B2Vec2,
29 | lvel box2d.B2Vec2,
30 | ang float64,
31 | avel float64,
32 | clr ColorScale) *Particle {
33 |
34 | return &Particle{
35 | img: flameParticleImg,
36 | pos: pos,
37 | lvel: lvel,
38 | ang: ang,
39 | avel: avel,
40 | age: 0,
41 | ttl: ttl,
42 | crl: clr,
43 | }
44 | }
45 |
46 | func (p *Particle) IsDead() bool {
47 | return p.age > p.ttl
48 | }
49 |
50 | func (p *Particle) IncAge() {
51 | p.age++
52 | }
53 |
54 | func (p *Particle) Update() {
55 | p.age++
56 | p.ang += p.avel
57 | p.pos.OperatorPlusInplace(p.lvel)
58 | }
59 |
60 | func (p *Particle) Draw(screen *ebiten.Image, cam Cam) {
61 | opts := &ebiten.DrawImageOptions{}
62 |
63 | bounds := p.img.Bounds()
64 | opts.GeoM.Translate(-float64(bounds.Max.X/2), -float64(bounds.Max.Y/2))
65 | opts.GeoM.Scale(1/float64(bounds.Max.X), 1/float64(bounds.Max.Y))
66 | opts.GeoM.Rotate(p.ang)
67 | opts.GeoM.Translate(p.pos.X, p.pos.Y)
68 | opts.GeoM.Translate(-cam.Pos.X, -cam.Pos.Y)
69 | opts.GeoM.Scale(cam.Zoom, cam.Zoom)
70 | opts.GeoM.Translate(ScreenWidth/2, ScreenHeight/2)
71 | opts.ColorM.Scale(p.crl.R, p.crl.G, p.crl.B, p.crl.A)
72 |
73 | screen.DrawImage(p.img, opts)
74 | }
75 |
76 | type ParticleSystem struct {
77 | particles map[*Particle]struct{}
78 | }
79 |
80 | func NewParticleSystem() *ParticleSystem {
81 | return &ParticleSystem{
82 | particles: make(map[*Particle]struct{}),
83 | }
84 | }
85 |
86 | func (ps *ParticleSystem) Emit(pos box2d.B2Vec2, dir float64, angDisp float64) {
87 | count := RandInt(1, 100)
88 |
89 | for i := 0; i < count; i++ {
90 | ang := RandFloat(dir-angDisp/2, dir+angDisp/2)
91 | avel := RandFloat(-1.0, 1.0)
92 | speed := RandFloat(0.05, 0.2)
93 | ttl := RandInt(20, 50)
94 |
95 | c, s := math.Cos(ang), math.Sin(ang)
96 | lVel := box2d.MakeB2Vec2(c, s)
97 | lVel.OperatorScalarMulInplace(speed)
98 |
99 | rPos := box2d.B2Vec2Add(pos, box2d.MakeB2Vec2(RandFloat(-0.2, 0.2), RandFloat(-0.5, 0.5)))
100 | // TODO: fix colors: add more yellow
101 | clr := ColorScale{
102 | R: RandFloat(0.2, 0.5),
103 | G: RandFloat(0.05, 0.1),
104 | B: 0,
105 | A: RandFloat(0.5, 1),
106 | }
107 | p := NewParticle(ttl, rPos, lVel, ang, avel, clr)
108 | ps.particles[p] = struct{}{}
109 | }
110 | }
111 |
112 | func (ps *ParticleSystem) Update() {
113 | for p := range ps.particles {
114 | p.IncAge()
115 | p.Update()
116 | if p.IsDead() {
117 | delete(ps.particles, p)
118 | }
119 | }
120 | }
121 |
122 | func (ps *ParticleSystem) Draw(screen *ebiten.Image, cam Cam) {
123 | for particle := range ps.particles {
124 | particle.Draw(screen, cam)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/basic_parts.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | )
6 |
7 | type BasicPart struct {
8 | *GameObj
9 | }
10 |
11 | func (p *BasicPart) GetBody() *box2d.B2Body {
12 | return p.body
13 | }
14 |
15 | func (p *BasicPart) Update(Keys) {}
16 |
17 | func ConstructBasicPart(
18 | world *box2d.B2World,
19 | _ Tank,
20 | _ *ParticleSystem,
21 | shipPos box2d.B2Vec2, // TODO: create interface: IHavePosAndSize
22 | shipSize box2d.B2Vec2,
23 | pos box2d.B2Vec2,
24 | sprite Sprite,
25 | ang float64) Part {
26 |
27 | shipHalfSize := box2d.B2Vec2MulScalar(0.5, shipSize)
28 | worldPos := box2d.B2Vec2Add(shipPos, pos)
29 | worldPos = box2d.B2Vec2Add(worldPos, shipHalfSize.OperatorNegate())
30 | worldPos = box2d.B2Vec2Add(worldPos, box2d.MakeB2Vec2(0.5, 0.5))
31 |
32 | // TODO: fix ship pos. now we use ship pos twice
33 | //pos.OperatorPlusInplace(shipPos)
34 | //pos.OperatorPlusInplace(shipHalfSize.OperatorNegate())
35 | //pos.OperatorPlusInplace(box2d.MakeB2Vec2(0.5, 0.5))
36 | ////x := box2d.B2Vec2Add(shipPos, pos)
37 | //x := pos
38 | part := &BasicPart{GameObj: NewGameObj(
39 | world,
40 | sprite,
41 | worldPos,
42 | ang,
43 | 0,
44 | box2d.B2Vec2_zero,
45 | DefaultFriction, DefaultFixtureDensity, DefaultFixtureRestitution, true)}
46 | part.GetBody().SetUserData(part)
47 | return part
48 | }
49 |
50 | type CabinDef struct {
51 | Dir Direction
52 | }
53 |
54 | func (d CabinDef) Construct(
55 | world *box2d.B2World,
56 | tanker Tank,
57 | ps *ParticleSystem,
58 | shipPos box2d.B2Vec2,
59 | shipSize box2d.B2Vec2,
60 | pos box2d.B2Vec2) Part {
61 |
62 | return ConstructBasicPart(
63 | world,
64 | tanker,
65 | ps,
66 | shipPos,
67 | shipSize,
68 | pos,
69 | cabinSprite,
70 | d.Dir.GetAng())
71 | }
72 |
73 | type BoxDef struct {
74 | }
75 |
76 | func (d BoxDef) Construct(
77 | world *box2d.B2World,
78 | tanker Tank,
79 | ps *ParticleSystem,
80 | shipPos box2d.B2Vec2,
81 | shipSize box2d.B2Vec2,
82 | pos box2d.B2Vec2) Part {
83 |
84 | return ConstructBasicPart(
85 | world,
86 | tanker,
87 | ps,
88 | shipPos,
89 | shipSize,
90 | pos,
91 | boxSprite,
92 | 0)
93 | }
94 |
95 | type LegDef struct {
96 | Dir Direction
97 | }
98 |
99 | func (d LegDef) Construct(
100 | world *box2d.B2World,
101 | tanker Tank,
102 | ps *ParticleSystem,
103 | shipPos box2d.B2Vec2, // TODO: remove. use ship
104 | shipSize box2d.B2Vec2,
105 | pos box2d.B2Vec2) Part {
106 |
107 | return ConstructBasicPart(
108 | world,
109 | tanker,
110 | ps,
111 | shipPos,
112 | shipSize,
113 | pos,
114 | legSprite,
115 | d.Dir.GetAng())
116 | }
117 |
118 | type LegFasteningDef struct {
119 | Dir Direction
120 | }
121 |
122 | func (d LegFasteningDef) Construct(
123 | world *box2d.B2World,
124 | tanker Tank,
125 | ps *ParticleSystem,
126 | shipPos box2d.B2Vec2,
127 | shipSize box2d.B2Vec2,
128 | pos box2d.B2Vec2) Part {
129 |
130 | return ConstructBasicPart(
131 | world,
132 | tanker,
133 | ps, shipPos,
134 | shipSize,
135 | pos,
136 | legFasteningSprite,
137 | d.Dir.GetAng())
138 | }
139 |
--------------------------------------------------------------------------------
/svg/path.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "github.com/ByteArena/box2d"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | type Path struct {
12 | ID string
13 | Title string
14 | Description string
15 | Verts []box2d.B2Vec2
16 | }
17 |
18 | func (p *Path) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
19 | var prj struct {
20 | ID string `xml:"id,attr"`
21 | Data string `xml:"d,attr"`
22 | Title string `xml:"title"`
23 | Description string `xml:"desc"`
24 | }
25 |
26 | if err := d.DecodeElement(&prj, &start); err != nil {
27 | return err
28 | }
29 |
30 | verts, err := parsePath(prj.Data)
31 | if err != nil {
32 | return err
33 | }
34 |
35 | *p = Path{
36 | ID: prj.ID,
37 | Title: prj.Title,
38 | Description: prj.Description,
39 | Verts: verts,
40 | }
41 | return nil
42 | }
43 |
44 | func parsePath(str string) ([]box2d.B2Vec2, error) {
45 | parts := strings.Split(str, " ")
46 |
47 | verts := make([]box2d.B2Vec2, 0)
48 | cmd := ""
49 | var prev *box2d.B2Vec2 = nil
50 | for i := 0; i < len(parts); i++ {
51 | switch parts[i] {
52 | case "M", "m", "L", "l", "V", "v", "H", "h":
53 | cmd = parts[i]
54 | case "Z", "z": // we finish here because don't need 8like shapes
55 | return verts, nil
56 | default:
57 | var v box2d.B2Vec2
58 | switch cmd {
59 | case "M", "L":
60 | _v, err := parseCoords(parts[i])
61 | if err != nil {
62 | return nil, err
63 | }
64 | v = _v
65 | case "m", "l":
66 | _v, err := parseCoords(parts[i])
67 | if err != nil {
68 | return nil, err
69 | }
70 | if prev == nil {
71 | v = _v
72 | } else {
73 | v = box2d.MakeB2Vec2(prev.X+_v.X, prev.Y+_v.Y)
74 | }
75 | case "V":
76 | y, err := strconv.ParseFloat(parts[i], 64)
77 | if err != nil {
78 | return nil, err
79 | }
80 | v = box2d.MakeB2Vec2(prev.X, y)
81 | case "v":
82 | y, err := strconv.ParseFloat(parts[i], 64)
83 | if err != nil {
84 | return nil, err
85 | }
86 | v = box2d.MakeB2Vec2(prev.X, prev.Y+y)
87 | case "H":
88 | x, err := strconv.ParseFloat(parts[i], 64)
89 | if err != nil {
90 | return nil, err
91 | }
92 | v = box2d.MakeB2Vec2(x, prev.Y)
93 | case "h":
94 | x, err := strconv.ParseFloat(parts[i], 64)
95 | if err != nil {
96 | return nil, err
97 | }
98 | v = box2d.MakeB2Vec2(prev.X+x, prev.Y)
99 | }
100 |
101 | prev = &v
102 | verts = append(verts, v)
103 | }
104 | }
105 |
106 | return verts, nil
107 | }
108 |
109 | func parseCoords(str string) (box2d.B2Vec2, error) {
110 | parts := strings.Split(str, ",")
111 | if len(parts) != 2 {
112 | return box2d.B2Vec2{}, fmt.Errorf("2 coords expected in %v", str)
113 | }
114 |
115 | x, err := strconv.ParseFloat(parts[0], 64)
116 | if err != nil {
117 | return box2d.B2Vec2{}, err
118 | }
119 |
120 | y, err := strconv.ParseFloat(parts[1], 64)
121 | if err != nil {
122 | return box2d.B2Vec2{}, err
123 | }
124 |
125 | return box2d.MakeB2Vec2(x, y), nil
126 | }
127 |
--------------------------------------------------------------------------------
/crane_jaws.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "math"
7 | "time"
8 | )
9 |
10 | const (
11 | motorSpeed = 10
12 | )
13 |
14 | type CraneJaws struct {
15 | motor *box2d.B2RevoluteJoint
16 | upper, lower *GameObj
17 | lastControlled time.Time
18 | }
19 |
20 | func NewCraneJaws(c *Crane) *CraneJaws {
21 | density := 5.0
22 | upper := NewGameObj(
23 | c.world,
24 | craneUpperJawSprite,
25 | box2d.B2Vec2Add(c.GetPos(), box2d.MakeB2Vec2(0, 0.5)),
26 | DirectionDown.GetAng(), 0,
27 | box2d.B2Vec2_zero,
28 | 1, density, 0.0, true)
29 | lower := NewGameObj(
30 | c.world,
31 | craneLowerJawSprite,
32 | box2d.B2Vec2Add(c.GetPos(), box2d.MakeB2Vec2(0, 0.5)),
33 | DirectionDown.GetAng(), 0,
34 | box2d.B2Vec2_zero,
35 | 1, density, 0.0, true)
36 |
37 | lower.body.SetGravityScale(40)
38 | upper.body.SetGravityScale(40)
39 |
40 | // motor joint
41 | rjd := box2d.MakeB2RevoluteJointDef()
42 | rjd.BodyA = upper.body
43 | rjd.LocalAnchorA = box2d.MakeB2Vec2(-0.5, 0)
44 | rjd.BodyB = lower.body
45 | rjd.LocalAnchorB = box2d.MakeB2Vec2(-0.5, 0)
46 | rjd.CollideConnected = false
47 | rjd.EnableMotor = true
48 | rjd.EnableLimit = true
49 | rjd.UpperAngle = math.Pi / 2
50 | rjd.LowerAngle = -math.Pi / 4
51 | rjd.MaxMotorTorque = 300
52 | m := c.world.CreateJoint(&rjd)
53 |
54 | // joint to last chain element (upper)
55 | cjd := box2d.MakeB2RevoluteJointDef()
56 | cjd.BodyA = c.chain[len(c.chain)-1].body
57 | cjd.LocalAnchorA = box2d.MakeB2Vec2(0, c.chainElSize.Y/2)
58 | cjd.BodyB = upper.body
59 | cjd.LocalAnchorB = box2d.MakeB2Vec2(-0.5, 0)
60 | cjd.CollideConnected = false
61 | cjd.EnableMotor = true
62 | c.world.CreateJoint(&cjd)
63 |
64 | // joint to last chain element (lower)
65 | cjd = box2d.MakeB2RevoluteJointDef()
66 | cjd.BodyA = c.chain[len(c.chain)-1].body
67 | cjd.LocalAnchorA = box2d.MakeB2Vec2(0, c.chainElSize.Y/2)
68 | cjd.BodyB = lower.body
69 | cjd.LocalAnchorB = box2d.MakeB2Vec2(-0.5, 0)
70 | cjd.CollideConnected = false
71 | cjd.EnableMotor = true
72 | c.world.CreateJoint(&cjd)
73 |
74 | return &CraneJaws{
75 | motor: m.(*box2d.B2RevoluteJoint),
76 | upper: upper,
77 | lower: lower,
78 | }
79 | }
80 |
81 | func (j *CraneJaws) Draw(screen *ebiten.Image, cam Cam) {
82 | j.upper.Draw(screen, cam)
83 | j.lower.Draw(screen, cam)
84 | }
85 |
86 | func (j *CraneJaws) Update() {
87 | //a := j.motor.GetJointAngle()
88 | //if FloatEquals(a, math.Pi/2) && FloatEquals(j.motor.GetMotorSpeed(), 0.0) {
89 | // // TODO: crane is open: create angle joint
90 | //}
91 |
92 | if j.lastControlled.Add(time.Second).After(time.Now()) {
93 | return
94 | }
95 | if j.motor.GetMotorSpeed() > 0 {
96 | j.motor.SetMotorSpeed(0)
97 | }
98 | }
99 |
100 | func (j *CraneJaws) OpenClose() {
101 | if j.lastControlled.Add(time.Second / 5).After(time.Now()) {
102 | return
103 | }
104 | j.lastControlled = time.Now()
105 |
106 | ms := j.motor.GetMotorSpeed()
107 | if ms < 0 {
108 | j.motor.SetMotorSpeed(motorSpeed)
109 | return
110 | }
111 |
112 | j.motor.SetMotorSpeed(-motorSpeed)
113 | }
114 |
--------------------------------------------------------------------------------
/level.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2/text"
6 | svg2 "go-space-crane/svg"
7 | "gopkg.in/yaml.v3"
8 | "io/ioutil"
9 | "path"
10 | "strconv"
11 | )
12 |
13 | type LevelDef struct {
14 | Name string
15 | Description string
16 | Terrain string
17 | Ship ShipDef
18 | TaskDefs []string `yaml:"tasks"`
19 | }
20 |
21 | // TODO: get rid of level yaml. Move it to SVG!
22 | type Level struct {
23 | Ship *Ship
24 | Terrain *Terrain
25 | Platforms map[string]*Platform
26 | Cargos map[string]*Cargo
27 | Tasks []Task
28 | bounds box2d.B2AABB
29 | }
30 |
31 | func LoadLevel(world *box2d.B2World, ps *ParticleSystem, name string) Level {
32 | levelDefData, err := ioutil.ReadFile(path.Join(LevelsDir, name))
33 | checkErr(err)
34 | var levelDef LevelDef
35 | checkErr(yaml.Unmarshal(levelDefData, &levelDef))
36 |
37 | // this svg holds terrain data, ship position, cargos position, platforms data
38 | svg, err := svg2.Load(path.Join(AssetsDir, TerrainsDir, levelDef.Terrain+".svg"))
39 | checkErr(err)
40 |
41 | // TODO: also load rects
42 | // For now all terrain data stored in pathes
43 | terrainVertsSet := make([][]box2d.B2Vec2, len(svg.Layers[0].Pathes))
44 | for i, path := range svg.Layers[0].Pathes {
45 | terrainVertsSet[i] = path.Verts
46 | }
47 |
48 | // Platforms are rects with "platform" title
49 | platforms := make(map[string]*Platform)
50 | levelBounds := box2d.MakeB2AABB()
51 | for _, rect := range svg.Layers[0].Rects {
52 | switch rect.Title {
53 | case "platform":
54 | fuel, err := strconv.Atoi(rect.Description)
55 | checkErr(err)
56 | platform := NewPlatform(
57 | rect.ID,
58 | world,
59 | box2d.B2Vec2Add(rect.Pos, box2d.B2Vec2MulScalar(0.5, rect.Size)),
60 | rect.Size,
61 | float64(fuel))
62 | platforms[platform.id] = platform
63 | case "bounds":
64 | levelBounds.LowerBound = box2d.MakeB2Vec2(rect.Pos.X, rect.Pos.Y)
65 | levelBounds.UpperBound = box2d.MakeB2Vec2(rect.Pos.X+rect.Size.X, rect.Pos.Y+rect.Size.Y)
66 | }
67 | }
68 |
69 | // Cargos are ellipses with "cargo" title
70 | // Ship default position stored in ellipse with "ship" title
71 | cargos := make(map[string]*Cargo)
72 | var shipPos box2d.B2Vec2
73 | for _, ellipse := range svg.Layers[0].Ellipses {
74 | switch ellipse.Title {
75 | case "cargo":
76 | cargo := NewCargo(ellipse.ID, world, ellipse.Pos)
77 | cargos[cargo.id] = cargo
78 | case "ship":
79 | shipPos = ellipse.Pos
80 | }
81 | }
82 |
83 | if levelDef.Ship.Pos != nil {
84 | shipPos = *levelDef.Ship.Pos
85 | }
86 | ship := LoadShip(world, shipPos, ps, levelDef.Ship)
87 |
88 | tasks := make([]Task, len(levelDef.TaskDefs))
89 | for i, def := range levelDef.TaskDefs {
90 | tasks[i] = ParseTaskDef(def, platforms, cargos)
91 | text.CacheGlyphs(hoodFace, tasks[i].TargetName())
92 | }
93 |
94 | for _, cargo := range cargos {
95 | cargo.tasks = tasks
96 | }
97 |
98 | return Level{
99 | Ship: ship,
100 | Terrain: NewTerrain(world, terrainVertsSet),
101 | Platforms: platforms,
102 | Cargos: cargos,
103 | Tasks: tasks,
104 | bounds: levelBounds,
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/platform.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/text"
7 | "image/color"
8 | )
9 |
10 | type Platform struct {
11 | *GameObj
12 | id string
13 | fuel float64
14 | maxFuel float64
15 | ship *Ship
16 | size box2d.B2Vec2
17 | }
18 |
19 | func NewPlatform(id string, world *box2d.B2World, pos box2d.B2Vec2, size box2d.B2Vec2, fuel float64) *Platform {
20 | verts := []box2d.B2Vec2{
21 | box2d.MakeB2Vec2(-size.X/2, -size.Y/2),
22 | box2d.MakeB2Vec2(size.X/2, -size.Y/2),
23 | box2d.MakeB2Vec2(size.X/2, size.Y/2),
24 | box2d.MakeB2Vec2(-size.X/2, size.Y/2),
25 | }
26 |
27 | gameObj := NewGameObj(
28 | world,
29 | NewSprite(emptyTransparentImage, [][]box2d.B2Vec2{verts}),
30 | pos,
31 | 0,
32 | 0,
33 | box2d.B2Vec2_zero,
34 | DefaultFriction,
35 | DefaultFixtureDensity,
36 | DefaultFixtureRestitution, false)
37 |
38 | platform := &Platform{
39 | GameObj: gameObj,
40 | id: id,
41 | fuel: fuel,
42 | maxFuel: fuel,
43 | size: size,
44 | }
45 | platform.body.SetUserData(platform)
46 | return platform
47 | }
48 |
49 | func (p *Platform) Draw(screen *ebiten.Image, cam Cam) {
50 | p.GameObj.Draw(screen, cam)
51 |
52 | // Draw platform
53 | func() {
54 | size := box2d.MakeB2Vec2(p.size.X*cam.Zoom, p.size.Y*cam.Zoom)
55 |
56 | opts := &ebiten.DrawImageOptions{}
57 | pos := cam.Project(box2d.B2Vec2_zero, p.GetPos(), 0)
58 | opts.ColorM.Translate(0, 0, 0, 1)
59 | opts.GeoM.Scale(size.X, size.Y)
60 | opts.GeoM.Translate(pos.X-(size.X/2), pos.Y-(size.Y/2))
61 | screen.DrawImage(emptyTransparentImage, opts)
62 |
63 | }()
64 |
65 | // Draw fuel
66 | func() {
67 | size := box2d.MakeB2Vec2(p.size.X*cam.Zoom/2, p.size.Y*cam.Zoom/2)
68 | opts := &ebiten.DrawImageOptions{}
69 | pos := cam.Project(box2d.B2Vec2_zero, p.GetPos(), 0)
70 | opts.ColorM.Translate(1, 0, 0, 1)
71 | opts.GeoM.Scale(size.X, size.Y)
72 | opts.GeoM.Translate(pos.X-(size.X/2), pos.Y-(size.Y/2))
73 | screen.DrawImage(emptyTransparentImage, opts)
74 |
75 | opts = &ebiten.DrawImageOptions{}
76 | opts.ColorM.Translate(0, 1, 0, 1)
77 | opts.GeoM.Scale(Remap(p.fuel, 0, p.maxFuel, 0, size.X), size.Y)
78 | opts.GeoM.Translate(pos.X-(size.X/2), pos.Y-(size.Y/2))
79 | screen.DrawImage(emptyTransparentImage, opts)
80 | }()
81 |
82 | // Draw platformID
83 | func() {
84 | bounds := text.BoundString(platformFace, p.id)
85 | img := ebiten.NewImage(bounds.Max.X-bounds.Min.X, bounds.Max.Y-bounds.Min.Y)
86 |
87 | text.Draw(img, p.id, platformFace, -bounds.Min.X, -bounds.Min.Y, color.White)
88 | pos := p.GetPos()
89 | pos = box2d.B2Vec2Add(pos, box2d.MakeB2Vec2(0.5, 0))
90 | opts := &ebiten.DrawImageOptions{}
91 | opts.GeoM.Translate(-float64(bounds.Max.X/2), -float64(bounds.Max.Y/2))
92 | opts.GeoM.Scale(1/float64(100), 1/float64(100))
93 | opts.GeoM.Translate(pos.X-p.size.X/2, pos.Y)
94 | opts.GeoM.Translate(-cam.Pos.X, -cam.Pos.Y)
95 | opts.GeoM.Scale(cam.Zoom, cam.Zoom)
96 | opts.GeoM.Translate(ScreenWidth/2, ScreenHeight/2)
97 |
98 | //opts := &ebiten.DrawImageOptions{}
99 | //opts.GeoM.Translate(100,100)
100 |
101 | screen.DrawImage(img, opts)
102 | }()
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/world.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/ebitenutil"
7 | "image/color"
8 | "math"
9 | )
10 |
11 | func DrawDebugBody(screen *ebiten.Image, body *box2d.B2Body, cam Cam, clr color.Color) {
12 | for fix := body.GetFixtureList(); fix != nil; fix = fix.GetNext() {
13 | switch shape := fix.GetShape().(type) {
14 | case *box2d.B2CircleShape:
15 | DrawDebugCircleShape(screen, body, shape, cam, clr)
16 | case *box2d.B2PolygonShape:
17 | DrawDebugPolygonShape(screen, body, shape, cam, clr)
18 | case *box2d.B2ChainShape:
19 | DrawDebugChainShape(screen, body, shape, cam, clr)
20 | case *box2d.B2EdgeShape:
21 | DrawDebugEdgeShape(screen, body, shape, cam, clr)
22 | }
23 | }
24 |
25 | // TODO: draw angle
26 |
27 | // Center cross
28 | crossSize := 0.3
29 | pos := body.GetPosition()
30 | ang := body.GetAngle()
31 | p1 := box2d.MakeB2Vec2(-crossSize/2, 0)
32 | p2 := box2d.MakeB2Vec2(crossSize/2, 0)
33 | p3 := box2d.MakeB2Vec2(0, -crossSize/2)
34 | p4 := box2d.MakeB2Vec2(0, crossSize/2)
35 |
36 | p1 = cam.Project(p1, pos, ang)
37 | p2 = cam.Project(p2, pos, ang)
38 | p3 = cam.Project(p3, pos, ang)
39 | p4 = cam.Project(p4, pos, ang)
40 |
41 | crossClr := color.RGBA{
42 | R: 0,
43 | G: 0xff,
44 | B: 0,
45 | A: 0xff,
46 | }
47 | ebitenutil.DrawLine(screen, p1.X, p1.Y, p2.X, p2.Y, crossClr)
48 | ebitenutil.DrawLine(screen, p3.X, p3.Y, p4.X, p4.Y, crossClr)
49 | }
50 |
51 | func DrawDebugChainShape(screen *ebiten.Image, body *box2d.B2Body, shape *box2d.B2ChainShape, cam Cam, clr color.Color) {
52 | drawDebugPolyFromVerts(screen, body.GetPosition(), body.GetAngle(), shape.M_vertices[:shape.M_count], cam, clr)
53 | }
54 |
55 | func DrawDebugEdgeShape(screen *ebiten.Image, body *box2d.B2Body, shape *box2d.B2EdgeShape, cam Cam, clr color.Color) {
56 | v1 := cam.Project(shape.M_vertex1, body.GetPosition(), body.GetAngle())
57 | v2 := cam.Project(shape.M_vertex2, body.GetPosition(), body.GetAngle())
58 | ebitenutil.DrawLine(
59 | screen,
60 | v1.X, v1.Y,
61 | v2.X, v2.Y,
62 | clr)
63 | }
64 |
65 | func DrawDebugPolygonShape(screen *ebiten.Image, body *box2d.B2Body, shape *box2d.B2PolygonShape, cam Cam, clr color.Color) {
66 | drawDebugPolyFromVerts(screen, body.GetPosition(), body.GetAngle(), shape.M_vertices[:shape.M_count], cam, clr)
67 | }
68 |
69 | func DrawDebugCircleShape(screen *ebiten.Image, body *box2d.B2Body, shape *box2d.B2CircleShape, cam Cam, clr color.Color) {
70 | count := box2d.B2_maxPolygonVertices
71 | verts := make([]box2d.B2Vec2, count)
72 | for i := 0; i < count; i++ {
73 | ang := 2 * math.Pi * float64(i) / float64(count)
74 | r := shape.GetRadius()
75 | verts[i] = box2d.MakeB2Vec2(math.Cos(ang)*r, math.Sin(ang)*r)
76 | }
77 |
78 | polyShape := box2d.MakeB2PolygonShape()
79 | polyShape.Set(verts, len(verts))
80 | DrawDebugPolygonShape(screen, body, &polyShape, cam, clr)
81 | }
82 |
83 | // TODO: use box2d.B2Transform instead of pos+ang
84 | func drawDebugPolyFromVerts(screen *ebiten.Image, pos box2d.B2Vec2, ang float64, verts []box2d.B2Vec2, cam Cam, clr color.Color) {
85 | for i := 1; i < len(verts); i++ {
86 | v1 := cam.Project(verts[i], pos, ang)
87 | v2 := cam.Project(verts[i-1], pos, ang)
88 | ebitenutil.DrawLine(
89 | screen,
90 | v1.X, v1.Y,
91 | v2.X, v2.Y,
92 | clr)
93 | }
94 |
95 | first := cam.Project(verts[0], pos, ang)
96 | last := cam.Project(verts[len(verts)-1], pos, ang)
97 | ebitenutil.DrawLine(
98 | screen,
99 | first.X, first.Y,
100 | last.X, last.Y,
101 | clr)
102 | }
103 |
--------------------------------------------------------------------------------
/init.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
7 | "github.com/hajimehoshi/ebiten/v2/text"
8 | "golang.org/x/image/font"
9 | "golang.org/x/image/font/opentype"
10 | "image"
11 | "image/color"
12 | "math/rand"
13 | "time"
14 | )
15 |
16 | var boxSprite Sprite
17 | var engineSprite Sprite
18 | var legSprite Sprite
19 | var legFasteningSprite Sprite
20 | var cabinSprite Sprite
21 | var craneSprite Sprite
22 | var craneUpperJawSprite Sprite
23 | var craneLowerJawSprite Sprite
24 | var chainElSprite Sprite
25 | var cargoSprite Sprite
26 | var flameParticleImg *ebiten.Image
27 | var platformFace font.Face
28 | var cargoFace font.Face
29 | var hoodFace font.Face
30 | var radarArrowImg *ebiten.Image
31 | var hoodImg *ebiten.Image
32 | var modalImg *ebiten.Image
33 | var emptyTransparentImage = ebiten.NewImage(1, 1)
34 | var emptyImage = ebiten.NewImage(3, 3)
35 | var emptySubImage = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
36 | var modalTitleFace font.Face
37 | var modalTextFace font.Face
38 | var levelNameFace font.Face
39 | var levelDescFace font.Face
40 |
41 | func init() {
42 | initEbiten()
43 |
44 | emptyImage.Fill(color.White)
45 | rand.Seed(time.Now().UnixNano())
46 |
47 | f, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
48 | checkErr(err)
49 |
50 | platformFace, err = opentype.NewFace(f, &opentype.FaceOptions{
51 | Size: 30,
52 | DPI: FontDpi,
53 | Hinting: font.HintingFull,
54 | })
55 | checkErr(err)
56 |
57 | cargoFace, err = opentype.NewFace(f, &opentype.FaceOptions{
58 | Size: 100,
59 | DPI: FontDpi,
60 | Hinting: font.HintingFull,
61 | })
62 | checkErr(err)
63 |
64 | hoodFace, err = opentype.NewFace(f, &opentype.FaceOptions{
65 | Size: 30,
66 | DPI: FontDpi,
67 | Hinting: font.HintingFull,
68 | })
69 | checkErr(err)
70 |
71 | modalTitleFace, err = opentype.NewFace(f, &opentype.FaceOptions{
72 | Size: 50,
73 | DPI: FontDpi,
74 | Hinting: font.HintingFull,
75 | })
76 | checkErr(err)
77 |
78 | modalTextFace, err = opentype.NewFace(f, &opentype.FaceOptions{
79 | Size: 20,
80 | DPI: FontDpi,
81 | Hinting: font.HintingFull,
82 | })
83 | checkErr(err)
84 |
85 | levelNameFace, err = opentype.NewFace(f, &opentype.FaceOptions{
86 | Size: 30,
87 | DPI: FontDpi,
88 | Hinting: font.HintingFull,
89 | })
90 | checkErr(err)
91 |
92 | levelDescFace, err = opentype.NewFace(f, &opentype.FaceOptions{
93 | Size: 30,
94 | DPI: FontDpi,
95 | Hinting: font.HintingFull,
96 | })
97 | checkErr(err)
98 |
99 | // Ship parts
100 | boxSprite = LoadPart("box")
101 | engineSprite = LoadPart("engine")
102 | legSprite = LoadPart("leg")
103 | legFasteningSprite = LoadPart("leg_fastening")
104 | cabinSprite = LoadPart("cabin")
105 | craneSprite = LoadPart("crane")
106 | craneUpperJawSprite = LoadPart("crane_upper_jaw")
107 | craneLowerJawSprite = LoadPart("crane_lower_jaw")
108 | chainElSprite = LoadPart("chain_el")
109 |
110 | cargoSprite = LoadPart("cargo")
111 | flameParticleImg = loadImage("flame_particle.png")
112 |
113 | radarArrowImg = loadImage("hood/radar_arrow.png")
114 |
115 | hoodImg = loadImage("hood/hood.png")
116 | // Print text on image for future localization
117 | text.Draw(hoodImg, fmt.Sprintf("%v\n%v", FuelLabelText, EnergyLabelText), hoodFace, 50, 1000, color.White)
118 | text.Draw(hoodImg, fmt.Sprintf("%v\n%v", TargetLabelText, DistanceLabelText), hoodFace, 550, 1000, color.White)
119 |
120 | // Cache distance glyphs
121 | for i := 0; i < 50; i++ {
122 | text.CacheGlyphs(hoodFace, fmt.Sprintf(DistanceText, i))
123 | }
124 |
125 | modalImg = loadImage("modal.png")
126 | }
127 |
128 | func initEbiten() {
129 | ebiten.SetWindowSize(ScreenWidth, ScreenHeight)
130 | ebiten.SetWindowTitle("Space Crane")
131 | ebiten.SetWindowResizable(true)
132 | }
133 |
--------------------------------------------------------------------------------
/svg/parser_test.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/stretchr/testify/require"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func TestParseSvg(t *testing.T) {
11 | data := `
12 | `
33 |
34 | s, err := Parse(strings.NewReader(data))
35 | require.NoError(t, err)
36 | require.Equal(t, "layer_id", s.Layers[0].ID)
37 | require.Equal(t, "path_id", s.Layers[0].Pathes[0].ID)
38 | require.Equal(t, "title", s.Layers[0].Pathes[0].Title)
39 | require.Equal(t, "desc", s.Layers[0].Pathes[0].Description)
40 | require.Len(t, s.Layers[0].Pathes[0].Verts, 4)
41 | require.Equal(t, "rect_id", s.Layers[0].Rects[0].ID)
42 | require.Equal(t, "title", s.Layers[0].Rects[0].Title)
43 | require.Equal(t, "desc", s.Layers[0].Rects[0].Description)
44 | require.Equal(t, float64(1), s.Layers[0].Rects[0].Pos.X)
45 | require.Equal(t, float64(2), s.Layers[0].Rects[0].Pos.Y)
46 | require.Equal(t, float64(3), s.Layers[0].Rects[0].Size.X)
47 | require.Equal(t, float64(4), s.Layers[0].Rects[0].Size.Y)
48 |
49 | require.Equal(t, float64(0), s.Layers[0].Ellipses[0].Pos.X)
50 | require.Equal(t, float64(0), s.Layers[0].Ellipses[0].Pos.Y)
51 | require.Equal(t, float64(1), s.Layers[0].Ellipses[0].Radius.X)
52 | require.Equal(t, float64(1), s.Layers[0].Ellipses[0].Radius.Y)
53 | }
54 |
55 | func TestParsePath(t *testing.T) {
56 | verts, err := parsePath("M 324.59604,0.08457427 325.01891,0.16914854 509.13709,108.25506 598.10922,285.86103 507.44561,513.19666 279.77168,590.66669 91.340209,465.49677 1.3531883,290.59718 105.88698,108.59336 Z")
57 | require.NoError(t, err)
58 | require.Len(t, verts, 9)
59 |
60 | verts, err = parsePath("M 0,-600 560,120 480,300 140,380 H -140 L -480,300 -560,120 Z")
61 | require.NoError(t, err)
62 | require.Len(t, verts, 7)
63 | require.Equal(t, box2d.MakeB2Vec2(0, -600), verts[0])
64 | require.Equal(t, box2d.MakeB2Vec2(560, 120), verts[1])
65 | require.Equal(t, box2d.MakeB2Vec2(480, 300), verts[2])
66 | require.Equal(t, box2d.MakeB2Vec2(140, 380), verts[3])
67 | require.Equal(t, box2d.MakeB2Vec2(-140, 380), verts[4])
68 | require.Equal(t, box2d.MakeB2Vec2(-480, 300), verts[5])
69 | require.Equal(t, box2d.MakeB2Vec2(-560, 120), verts[6])
70 |
71 | verts, err = parsePath("M 1,2 l 1,1 z")
72 | require.NoError(t, err)
73 | require.Len(t, verts, 2)
74 | require.Equal(t, box2d.MakeB2Vec2(1, 2), verts[0])
75 | require.Equal(t, box2d.MakeB2Vec2(2, 3), verts[1])
76 |
77 | verts, err = parsePath("M 0,1 H 2")
78 | require.NoError(t, err)
79 | require.Len(t, verts, 2)
80 | require.Equal(t, box2d.MakeB2Vec2(0, 1), verts[0])
81 | require.Equal(t, box2d.MakeB2Vec2(2, 1), verts[1])
82 |
83 | verts, err = parsePath("M 0,800 560,80 1120,800 1020,980 680,1060 H 440 L 100,980 Z")
84 | require.NoError(t, err)
85 | require.Len(t, verts, 7)
86 |
87 | verts, err = parsePath("M 1,2 V 3")
88 | require.NoError(t, err)
89 | require.Len(t, verts, 2)
90 | require.Equal(t, box2d.MakeB2Vec2(1, 2), verts[0])
91 | require.Equal(t, box2d.MakeB2Vec2(1, 3), verts[1])
92 |
93 | verts, err = parsePath("M 1,2 3,4 5,6 7,8")
94 | require.NoError(t, err)
95 | require.Len(t, verts, 4)
96 | require.Equal(t, box2d.MakeB2Vec2(1, 2), verts[0])
97 | require.Equal(t, box2d.MakeB2Vec2(3, 4), verts[1])
98 | require.Equal(t, box2d.MakeB2Vec2(5, 6), verts[2])
99 | require.Equal(t, box2d.MakeB2Vec2(7, 8), verts[3])
100 |
101 | verts, err = parsePath("M 1,2 3,4 5,6 7,8 Z")
102 | require.NoError(t, err)
103 | require.Len(t, verts, 4)
104 | require.Equal(t, box2d.MakeB2Vec2(1, 2), verts[0])
105 | require.Equal(t, box2d.MakeB2Vec2(3, 4), verts[1])
106 | require.Equal(t, box2d.MakeB2Vec2(5, 6), verts[2])
107 | require.Equal(t, box2d.MakeB2Vec2(7, 8), verts[3])
108 | }
109 |
--------------------------------------------------------------------------------
/level_selector.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "github.com/hajimehoshi/ebiten/v2/inpututil"
7 | "github.com/hajimehoshi/ebiten/v2/text"
8 | "github.com/hajimehoshi/ebiten/v2/vector"
9 | "gopkg.in/yaml.v3"
10 | "image/color"
11 | "io/ioutil"
12 | "path"
13 | "time"
14 | )
15 |
16 | const ()
17 |
18 | type LevelSelector struct {
19 | levels []LevelDef
20 | fileNames map[int]string
21 | index int
22 |
23 | lastControlled time.Time
24 | }
25 |
26 | func NewLevelSelector() *LevelSelector {
27 | files, err := ioutil.ReadDir(LevelsDir)
28 | checkErr(err)
29 | levels := make([]LevelDef, len(files))
30 | fileNames := make(map[int]string)
31 | for i, file := range files {
32 | levelDefData, err := ioutil.ReadFile(path.Join(LevelsDir, file.Name()))
33 | checkErr(err)
34 | var levelDef LevelDef
35 | checkErr(yaml.Unmarshal(levelDefData, &levelDef))
36 | levels[i] = levelDef
37 | fileNames[i] = file.Name()
38 | }
39 | return &LevelSelector{
40 | levels: levels,
41 | fileNames: fileNames,
42 | lastControlled: time.Now(),
43 | }
44 | }
45 |
46 | func (ls *LevelSelector) Update() error {
47 | if time.Since(ls.lastControlled) < time.Second/10 {
48 | return nil
49 | }
50 | ls.lastControlled = time.Now()
51 |
52 | keys := KeysFromSlice(inpututil.AppendPressedKeys(nil))
53 |
54 | if keys.IsPressed(ebiten.KeyEnter) || keys.IsPressed(ebiten.KeyNumpadEnter) {
55 | return NewLevelSelected(ls.fileNames[ls.index])
56 | }
57 | if keys.IsPressed(ebiten.KeyArrowDown) {
58 | ls.index++
59 | if ls.index >= len(ls.levels) {
60 | ls.index = 0
61 | }
62 | }
63 | if keys.IsPressed(ebiten.KeyArrowUp) {
64 | ls.index--
65 | if ls.index < 0 {
66 | ls.index = len(ls.levels) - 1
67 | }
68 | }
69 | return nil
70 | }
71 |
72 | func (ls *LevelSelector) Draw(screen *ebiten.Image) {
73 | screen.Fill(color.White)
74 | opts := &ebiten.DrawTrianglesOptions{
75 | FillRule: ebiten.EvenOdd,
76 | }
77 |
78 | levelBoxSize := box2d.MakeB2Vec2(500, 100)
79 | levelBoxStartPos := box2d.MakeB2Vec2(100, 100)
80 | levelBoxDist := 30.0
81 | selectedBorderSize := float32(5.0)
82 |
83 | for i, level := range ls.levels {
84 | if i == ls.index {
85 | var path vector.Path
86 | x := float32(levelBoxStartPos.X)
87 | y := float32(levelBoxStartPos.Y + float64(i)*levelBoxSize.Y + float64(i)*levelBoxDist)
88 | path.MoveTo(x-selectedBorderSize, y-selectedBorderSize)
89 | path.LineTo(x+float32(levelBoxSize.X)+selectedBorderSize, y-selectedBorderSize)
90 | path.LineTo(x+float32(levelBoxSize.X)+selectedBorderSize, y+float32(levelBoxSize.Y)+selectedBorderSize)
91 | path.LineTo(x-selectedBorderSize, y+float32(levelBoxSize.Y)+selectedBorderSize)
92 | vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil)
93 | for i := range vs {
94 | vs[i].SrcX = 1
95 | vs[i].SrcY = 1
96 | vs[i].ColorR = 0.5
97 | vs[i].ColorG = 0.5
98 | vs[i].ColorB = 0.5
99 | }
100 | screen.DrawTriangles(vs, is, emptySubImage, opts)
101 |
102 | text.Draw(
103 | screen,
104 | level.Name,
105 | levelNameFace,
106 | 1200,
107 | int(levelBoxStartPos.X),
108 | color.Black)
109 | text.Draw(
110 | screen,
111 | level.Description,
112 | levelDescFace,
113 | 800,
114 | int(levelBoxStartPos.X)+100,
115 | color.Black)
116 | }
117 |
118 | var path vector.Path
119 | x := float32(levelBoxStartPos.X)
120 | y := float32(levelBoxStartPos.Y + float64(i)*levelBoxSize.Y + float64(i)*levelBoxDist)
121 | path.MoveTo(x, y)
122 | path.LineTo(x+float32(levelBoxSize.X), y)
123 | path.LineTo(x+float32(levelBoxSize.X), y+float32(levelBoxSize.Y))
124 | path.LineTo(x, y+float32(levelBoxSize.Y))
125 |
126 | vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil)
127 | for i := range vs {
128 | vs[i].SrcX = 1
129 | vs[i].SrcY = 1
130 | vs[i].ColorR = 0
131 | vs[i].ColorG = 0
132 | vs[i].ColorB = 0
133 | }
134 | screen.DrawTriangles(vs, is, emptySubImage, opts)
135 |
136 | bounds := text.BoundString(levelNameFace, level.Name)
137 | text.Draw(
138 | screen,
139 | level.Name,
140 | levelNameFace,
141 | int(x+float32(levelBoxSize.X)/2)-bounds.Max.X/2,
142 | int(y+float32(levelBoxSize.Y)/2)-bounds.Max.Y/2,
143 | color.White)
144 | }
145 |
146 | }
147 |
148 | func (ls *LevelSelector) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
149 | return ScreenWidth, ScreenHeight
150 | }
151 |
--------------------------------------------------------------------------------
/crane.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "time"
7 | )
8 |
9 | type CraneDef struct {
10 | Dir Direction
11 | }
12 |
13 | type Crane struct {
14 | *GameObj
15 | chain []*GameObj
16 | jaws *CraneJaws
17 | chainLastControlled time.Time
18 |
19 | chainElSize box2d.B2Vec2
20 | }
21 |
22 | func (d CraneDef) Construct(
23 | world *box2d.B2World,
24 | _ Tank,
25 | _ *ParticleSystem,
26 | shipPos box2d.B2Vec2,
27 | shipSize box2d.B2Vec2,
28 | pos box2d.B2Vec2) Part {
29 |
30 | // TODO: duplicate in basic_part
31 | shipHalfSize := box2d.B2Vec2MulScalar(0.5, shipSize)
32 | worldPos := box2d.B2Vec2Add(shipPos, pos)
33 | worldPos = box2d.B2Vec2Add(worldPos, shipHalfSize.OperatorNegate())
34 | worldPos = box2d.B2Vec2Add(worldPos, box2d.MakeB2Vec2(0.5, 0.5))
35 |
36 | crane := &Crane{
37 | GameObj: NewGameObj(
38 | world,
39 | craneSprite,
40 | worldPos,
41 | d.Dir.GetAng(), 0,
42 | box2d.B2Vec2_zero,
43 | DefaultFriction, DefaultFixtureDensity, DefaultFixtureRestitution, true),
44 | chainElSize: getShapeSize(chainElSprite.vertsSet[0]),
45 | }
46 | crane.GetBody().SetUserData(crane)
47 |
48 | crane.unwind()
49 | crane.jaws = NewCraneJaws(crane)
50 |
51 | return crane
52 | }
53 |
54 | func (c *Crane) Draw(screen *ebiten.Image, cam Cam) {
55 | c.jaws.Draw(screen, cam)
56 | c.GameObj.Draw(screen, cam)
57 | for _, chainEl := range c.chain {
58 | chainEl.Draw(screen, cam)
59 | }
60 | }
61 |
62 | func (c *Crane) GetBody() *box2d.B2Body {
63 | return c.body
64 | }
65 |
66 | func (c *Crane) Update(keys Keys) {
67 | c.jaws.Update()
68 |
69 | if keys.IsPressed(ebiten.KeyTab) {
70 | c.jaws.OpenClose()
71 | }
72 | if keys.IsPressed(ebiten.KeyQ) {
73 | if c.chainLastControlled.Add(time.Second / 5).After(time.Now()) {
74 | return
75 | }
76 | c.chainLastControlled = time.Now()
77 | c.windup()
78 | }
79 | if keys.IsPressed(ebiten.KeyA) {
80 | if c.chainLastControlled.Add(time.Second / 5).After(time.Now()) {
81 | return
82 | }
83 | c.chainLastControlled = time.Now()
84 | c.unwind()
85 | }
86 | }
87 |
88 | func (c *Crane) windup() {
89 | if len(c.chain) <= 1 {
90 | return
91 | }
92 |
93 | c.world.DestroyBody(c.chain[0].body)
94 | c.chain = c.chain[1:]
95 |
96 | f := box2d.MakeB2Vec2(0, 100)
97 | c.jaws.upper.body.ApplyForce(f, c.jaws.upper.body.GetPosition(), true)
98 | c.jaws.lower.body.ApplyForce(f, c.jaws.upper.body.GetPosition(), true)
99 |
100 | if len(c.chain) > 0 {
101 | // TODO: check if previous join destroyed by destroying its body
102 | // TODO: use part rotation. now it is hardcoded
103 | c.createChainJoint(c.body, box2d.B2Vec2_zero, c.chain[0].body, box2d.MakeB2Vec2(0, -c.chainElSize.Y/2))
104 | }
105 | }
106 |
107 | func (c *Crane) unwind() {
108 | // TODO: use angle (see engine)
109 | pos := box2d.B2Vec2Add(c.body.GetPosition(), box2d.MakeB2Vec2(0, 0.5+c.chainElSize.Y/2))
110 |
111 | // Chain must be massive (see density) to joint work well
112 | chainEl := NewGameObj(
113 | c.world,
114 | chainElSprite,
115 | pos, 0, 0,
116 | box2d.B2Vec2_zero,
117 | DefaultFriction, 100, DefaultFixtureRestitution, true)
118 | chainEl.body.SetGravityScale(0.1)
119 |
120 | if len(c.chain) > 0 {
121 | // TODO: apply additional force jaws
122 | prevBody := c.chain[0].body
123 | c.destroyCrainJoints(prevBody)
124 | c.createChainJoint(prevBody, box2d.MakeB2Vec2(0, -c.chainElSize.Y/2), chainEl.body, box2d.MakeB2Vec2(0, c.chainElSize.Y/2))
125 | }
126 | // TODO: use rotation. now its hardcoded
127 | c.createChainJoint(c.body, box2d.B2Vec2_zero, chainEl.body, box2d.MakeB2Vec2(0, -c.chainElSize.Y/2))
128 |
129 | // TODO: use linked list?
130 | c.chain = append([]*GameObj{chainEl}, c.chain...)
131 | }
132 |
133 | func (c *Crane) createChainJoint(
134 | bodyA *box2d.B2Body,
135 | lpA box2d.B2Vec2,
136 | bodyB *box2d.B2Body,
137 | lpB box2d.B2Vec2) {
138 |
139 | //// TODO: try ropeJoint
140 | rjd := box2d.MakeB2RevoluteJointDef()
141 | rjd.BodyA = bodyA
142 | rjd.LocalAnchorA = lpA
143 | rjd.BodyB = bodyB
144 | rjd.LocalAnchorB = lpB
145 | rjd.CollideConnected = false
146 | c.world.CreateJoint(&rjd)
147 |
148 | //djd := box2d.MakeB2DistanceJointDef()
149 | //djd.BodyA = bodyA
150 | //djd.LocalAnchorA = lpA
151 | //djd.BodyB = bodyB
152 | //djd.LocalAnchorB = lpB
153 | //djd.CollideConnected = false
154 | //djd.Length = chainElLen
155 | //c.world.CreateJoint(&djd)
156 | }
157 |
158 | func (c *Crane) destroyCrainJoints(body *box2d.B2Body) {
159 | type IHaveBodyA interface {
160 | GetBodyA() *box2d.B2Body
161 | }
162 |
163 | for joint := body.GetJointList(); joint != nil; joint = joint.Next {
164 | ja, ok := joint.Joint.(IHaveBodyA)
165 | if ok && ja.GetBodyA() == c.GetBody() {
166 | c.world.DestroyJoint(joint.Joint)
167 | continue
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/assets/terrains/flat.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
101 |
--------------------------------------------------------------------------------
/ship.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ByteArena/box2d"
5 | "github.com/hajimehoshi/ebiten/v2"
6 | "gopkg.in/yaml.v3"
7 | "io/ioutil"
8 | "math"
9 | "path"
10 | )
11 |
12 | const (
13 | // TODO: 50 is for dummies. Set to 20
14 | ShipImpulseDestructionThreshold = 30
15 | )
16 |
17 | type ShipDef struct {
18 | Name string
19 | Energy float64
20 | Fuel float64
21 | MaxFuel float64
22 | Pos *box2d.B2Vec2
23 | }
24 |
25 | func LoadShip(world *box2d.B2World, pos box2d.B2Vec2, ps *ParticleSystem, def ShipDef) *Ship {
26 | data, err := ioutil.ReadFile(path.Join(ShipsDir, def.Name+".yaml"))
27 | checkErr(err)
28 |
29 | var partDefStrs [][]*string
30 | checkErr(yaml.Unmarshal(data, &partDefStrs))
31 |
32 | pDefs := make([][]PartDef, len(partDefStrs))
33 | for y, row := range partDefStrs {
34 | pDefRow := make([]PartDef, len(row))
35 | for x, pds := range row {
36 | pDefRow[x] = ParsePartDef(pds)
37 | }
38 | pDefs[y] = pDefRow
39 | }
40 |
41 | return NewShip(world, ps, pos, def, pDefs)
42 | }
43 |
44 | type Ship struct {
45 | world *box2d.B2World
46 | parts []Part
47 | size box2d.B2Vec2
48 | // This is the angle for the first part
49 | // We need it to calculate ship orientation. At init it must be 0
50 | originalAng float64
51 |
52 | energy float64
53 | maxEnergy float64
54 | fuel float64
55 | maxFuel float64
56 | isDestroyed bool
57 |
58 | contactPlatform *Platform
59 | }
60 |
61 | func NewShip(
62 | world *box2d.B2World,
63 | ps *ParticleSystem,
64 | pos box2d.B2Vec2,
65 | def ShipDef,
66 | partDefs [][]PartDef) *Ship {
67 |
68 | shipSize := box2d.MakeB2Vec2(float64(len(partDefs[0])), float64(len(partDefs)))
69 |
70 | ship := &Ship{
71 | world: world,
72 | size: shipSize,
73 | energy: def.Energy,
74 | maxEnergy: def.Energy,
75 | fuel: def.Fuel,
76 | maxFuel: def.MaxFuel,
77 | }
78 |
79 | parts := make([]Part, 0)
80 | iparts := make([][]Part, len(partDefs))
81 | for y, row := range partDefs {
82 | iparts[y] = make([]Part, len(row))
83 | for x, partDef := range row {
84 | if partDef != nil {
85 | part := partDef.Construct(
86 | world,
87 | ship,
88 | ps,
89 | pos,
90 | shipSize,
91 | box2d.MakeB2Vec2(float64(x), float64(y)))
92 | parts = append(parts, part)
93 | iparts[y][x] = part
94 | }
95 | }
96 | }
97 |
98 | // Create Weld joints to upper and left parts
99 | // TODO: dont need GetLeftPart, GetUpperPart. Can cache in loop
100 | for y, row := range iparts {
101 | for x, part := range row {
102 | if part != nil {
103 | // TODO a lot of dupes
104 | // Simplify and move to func
105 | // TODO: for now joins are not hard enough
106 | // Try to add joints to upped and lower parts and maybe add some more joints
107 | // TODO: try to simplify with distance+revolute joint
108 |
109 | if other := GetLeftPart(iparts, x, y); other != nil {
110 | jd := box2d.MakeB2WeldJointDef()
111 | jd.CollideConnected = false
112 | jd.BodyA = part.GetBody()
113 | jd.BodyB = other.GetBody()
114 | jd.ReferenceAngle = other.GetAng() - part.GetAng()
115 |
116 | rotA := box2d.NewB2RotFromAngle(math.Pi - part.GetAng())
117 | jd.LocalAnchorA = box2d.MakeB2Vec2(rotA.C/2, rotA.S/2)
118 | rotB := box2d.NewB2RotFromAngle(0 - other.GetAng())
119 | jd.LocalAnchorB = box2d.MakeB2Vec2(rotB.C/2, rotB.S/2)
120 |
121 | world.CreateJoint(&jd)
122 | }
123 |
124 | if other := GetUpperPart(iparts, x, y); other != nil {
125 | jd := box2d.MakeB2WeldJointDef()
126 | jd.CollideConnected = false
127 | jd.BodyA = part.GetBody()
128 | jd.BodyB = other.GetBody()
129 | jd.ReferenceAngle = other.GetAng() - part.GetAng()
130 |
131 | rotA := box2d.NewB2RotFromAngle(-math.Pi/2 - part.GetAng())
132 | jd.LocalAnchorA = box2d.MakeB2Vec2(rotA.C/2, rotA.S/2)
133 | rotB := box2d.NewB2RotFromAngle(math.Pi/2 - other.GetAng())
134 | jd.LocalAnchorB = box2d.MakeB2Vec2(rotB.C/2, rotB.S/2)
135 |
136 | world.CreateJoint(&jd)
137 | }
138 | }
139 | }
140 | }
141 |
142 | ship.parts = parts
143 | ship.originalAng = parts[0].GetAng() // TODO: Use cabin for ship center and angle!
144 |
145 | return ship
146 | }
147 |
148 | func (s *Ship) GetFuel() float64 {
149 | return s.fuel
150 | }
151 |
152 | func (s *Ship) ReduceFuel(val float64) {
153 | s.fuel -= val
154 | if s.fuel < 0 {
155 | s.fuel = 0
156 | }
157 | }
158 |
159 | func (s *Ship) destroy() {
160 | for _, part := range s.parts {
161 | for je := part.GetBody().GetJointList(); je != nil; je = je.Next {
162 | s.world.DestroyJoint(je.Joint)
163 | }
164 | }
165 | s.isDestroyed = true
166 | }
167 |
168 | // GetLandedPlatform returns current landed platform or nil if ship is not landed
169 | // Ship is landed when it has contact with platform and zero velocity
170 | // TODO: also check orientation!
171 | func (s *Ship) GetLandedPlatform() *Platform {
172 | if s.contactPlatform != nil && FloatEquals(s.GetVel(), 0) {
173 | return s.contactPlatform
174 | }
175 | return nil
176 | }
177 |
178 | func (s *Ship) ApplyForce(force box2d.B2Vec2) {
179 | for _, part := range s.parts {
180 | body := part.GetBody()
181 | body.ApplyForce(force, body.GetPosition(), true)
182 | }
183 | }
184 |
185 | func (s *Ship) GetPos() box2d.B2Vec2 {
186 | // TODO: calc cabin pos
187 | return s.parts[0].GetPos()
188 | }
189 |
190 | func (s *Ship) GetAng() float64 {
191 | return s.parts[0].GetAng() - s.originalAng
192 | }
193 |
194 | func (s *Ship) GetVel() float64 {
195 | for _, part := range s.parts {
196 | return part.GetVel()
197 | }
198 | panic("ship have no parts")
199 | }
200 |
201 | func (s *Ship) GetVelVec() box2d.B2Vec2 {
202 | for _, part := range s.parts {
203 | return part.GetVelVec()
204 | }
205 | panic("ship have no parts")
206 | }
207 |
208 | func (s *Ship) Update(keys Keys) {
209 | for _, part := range s.parts {
210 | part.Update(keys)
211 | }
212 |
213 | if platform := s.GetLandedPlatform(); platform != nil {
214 | // Refueling
215 | if s.fuel < s.maxFuel && platform.fuel > 0 {
216 | s.contactPlatform.fuel -= 10
217 | s.fuel += 10
218 | }
219 | }
220 |
221 | // Destroy ship if no energy
222 | if s.energy <= 0 {
223 | s.destroy()
224 | }
225 | }
226 |
227 | func (s *Ship) Draw(screen *ebiten.Image, cam Cam) {
228 | for _, part := range s.parts {
229 | part.Draw(screen, cam)
230 | }
231 | }
232 |
233 | func GetLeftPart(pds [][]Part, x, y int) Part {
234 | if x == 0 {
235 | return nil
236 | }
237 | return pds[y][x-1]
238 | }
239 |
240 | func GetUpperPart(pds [][]Part, x, y int) Part {
241 | if y == 0 {
242 | return nil
243 | }
244 | return pds[y-1][x]
245 | }
246 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
2 | github.com/ByteArena/box2d v1.0.2 h1:f7f9KEQWhCs1n516DMLzi5w6u0MeeE78Mes4fWMcj9k=
3 | github.com/ByteArena/box2d v1.0.2/go.mod h1:LzEuxY9iCz+tskfWCY3o0ywYBRafDDugdSj+/YGI6sE=
4 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
7 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
8 | github.com/hajimehoshi/bitmapfont/v2 v2.1.3 h1:JefUkL0M4nrdVwVq7MMZxSTh6mSxOylm+C4Anoucbb0=
9 | github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs=
10 | github.com/hajimehoshi/ebiten/v2 v2.2.0 h1:2mP9HrLLqiH9X3MajElYZEjVZU/CGh22iFkjatxhT4w=
11 | github.com/hajimehoshi/ebiten/v2 v2.2.0/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0=
12 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
13 | github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
14 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
15 | github.com/hajimehoshi/oto/v2 v2.1.0-alpha.2/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ=
16 | github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
17 | github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4=
18 | github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
19 | github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
20 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
21 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
25 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
26 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
27 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
28 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
30 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
31 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
32 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
33 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
34 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
35 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
36 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
37 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
38 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
39 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
40 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
41 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
42 | golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 h1:peBP2oZO/xVnGMaWMCyFEI0WENsGj71wx5K12mRELHQ=
43 | golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5/go.mod h1:c4YKU3ZylDmvbw+H/PSvm42vhdWbuxCzbonauEAP9B8=
44 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
45 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
46 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
48 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
49 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
50 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
51 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
52 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
53 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
55 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
56 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
57 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
58 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
59 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
60 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
61 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
62 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
63 | golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
64 | golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
65 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
67 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
68 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
69 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
70 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
71 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
72 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
73 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
74 | golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
75 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
76 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
77 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
80 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
81 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
82 |
--------------------------------------------------------------------------------
/assets/terrains/long_trip.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
175 |
--------------------------------------------------------------------------------
/game.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/ByteArena/box2d"
6 | "github.com/hajimehoshi/ebiten/v2"
7 | "github.com/hajimehoshi/ebiten/v2/ebitenutil"
8 | "github.com/hajimehoshi/ebiten/v2/inpututil"
9 | "github.com/hajimehoshi/ebiten/v2/text"
10 | "image/color"
11 | )
12 |
13 | type Game struct {
14 | world *box2d.B2World
15 | cam *Cam
16 | ship *Ship
17 | terrain *Terrain
18 | background Background
19 | ps *ParticleSystem
20 | platforms map[string]*Platform
21 | cargos map[string]*Cargo
22 | tasks []Task
23 | bounds box2d.B2AABB
24 |
25 | endModal *Modal
26 | err error
27 |
28 | // Optimizations
29 | prevTargetDistance int
30 | prevTargetDistanceImg *ebiten.Image
31 | prevTargetName string
32 | prevTargetNameImg *ebiten.Image
33 | }
34 |
35 | func NewGame(
36 | world *box2d.B2World,
37 | cam *Cam,
38 | background Background,
39 | ps *ParticleSystem, level Level) *Game {
40 |
41 | return &Game{
42 | world: world,
43 | cam: cam,
44 | ship: level.Ship,
45 | terrain: level.Terrain,
46 | background: background,
47 | ps: ps,
48 | platforms: level.Platforms,
49 | cargos: level.Cargos,
50 | tasks: level.Tasks,
51 | bounds: level.bounds,
52 | // Uncomment to debug modal
53 | //endModal: NewModal(LevelFailedText, PressEnterToContinue, Keys{ebiten.KeyEnter: struct{}{}, ebiten.KeyNumpadEnter: struct{}{}}),
54 | //err: levelComplete,
55 | }
56 | }
57 |
58 | func (g *Game) Update() error {
59 | keys := KeysFromSlice(inpututil.AppendPressedKeys(nil))
60 |
61 | if keys.IsPressed(ebiten.KeyEscape) {
62 | if g.endModal == nil {
63 | g.err = levelFailed
64 | g.endModal = NewModal(LevelExitText, PressEnterToContinue, Keys{ebiten.KeyEnter: struct{}{}, ebiten.KeyNumpadEnter: struct{}{}})
65 | }
66 | }
67 |
68 | if g.endModal != nil {
69 | if g.endModal.isClosed {
70 | return g.err
71 | }
72 | g.endModal.Update(keys)
73 | }
74 |
75 | if g.ship.isDestroyed || g.ship.GetFuel() <= 0 {
76 | if g.endModal == nil {
77 | g.err = levelFailed
78 | g.endModal = NewModal(LevelFailedText, PressEnterToContinue, Keys{ebiten.KeyEnter: struct{}{}, ebiten.KeyNumpadEnter: struct{}{}})
79 | }
80 | }
81 |
82 | // Tasks
83 | if len(g.tasks) == 0 {
84 | if g.endModal == nil {
85 | g.err = levelComplete
86 | g.endModal = NewModal(LevelCompleteText, PressEnterToContinue, Keys{ebiten.KeyEnter: struct{}{}, ebiten.KeyNumpadEnter: struct{}{}})
87 | }
88 | } else {
89 | if g.tasks[0].IsComplete() {
90 | g.tasks = g.tasks[1:]
91 | }
92 |
93 | for _, task := range g.tasks {
94 | // is landed
95 | if platform := g.ship.GetLandedPlatform(); platform != nil {
96 | task.ShipLanded(g.ship.contactPlatform)
97 | }
98 | }
99 | }
100 |
101 | // TODO: maxZoom must depends on ship size. For large ships
102 | // it is not always posible to see platform (see rocket)
103 |
104 | // Cam follows ship
105 | // Zoom level depends on ship speed
106 | targetZoom := MinCamZoom
107 | if !g.ship.isDestroyed {
108 | g.cam.Pos = g.ship.GetPos()
109 | targetZoom = MaxCamZoom - g.ship.GetVel()*20
110 | if targetZoom <= MinCamZoom {
111 | targetZoom = MinCamZoom
112 | }
113 | if targetZoom > MaxCamZoom {
114 | targetZoom = MaxCamZoom
115 | }
116 | }
117 | g.cam.Zoom += (targetZoom - g.cam.Zoom) / 100
118 |
119 | g.checkWorldBounds()
120 |
121 | g.ps.Update()
122 |
123 | if g.endModal == nil {
124 | g.ship.Update(keys)
125 | }
126 |
127 | for _, cargo := range g.cargos {
128 | cargo.Update()
129 | }
130 |
131 | g.world.Step(1.0/60.0, 8, 3)
132 | return nil
133 | }
134 |
135 | // TODO: apply force depends on ship impulse just to stop it
136 | func (g *Game) checkWorldBounds() {
137 | //force := 50.0
138 | shipPos := g.ship.GetPos()
139 | shipVel := g.ship.GetVelVec()
140 | mult := 10.0
141 |
142 | force := box2d.B2Vec2_zero
143 | if shipPos.X < g.bounds.LowerBound.X {
144 | force = box2d.B2Vec2Add(force, box2d.MakeB2Vec2(-shipVel.X*(g.bounds.LowerBound.X-shipPos.X)*mult, 0))
145 | }
146 | if shipPos.Y < g.bounds.LowerBound.Y {
147 | force = box2d.B2Vec2Add(force, box2d.MakeB2Vec2(0, -shipVel.Y*(g.bounds.LowerBound.Y-shipPos.Y)*mult))
148 | }
149 | if shipPos.X > g.bounds.UpperBound.X {
150 | force = box2d.B2Vec2Add(force, box2d.MakeB2Vec2(-shipVel.X*(shipPos.X-g.bounds.UpperBound.X)*mult, 0))
151 | }
152 | if shipPos.Y > g.bounds.UpperBound.Y {
153 | force = box2d.B2Vec2Add(force, box2d.MakeB2Vec2(0, -shipVel.Y*(shipPos.Y-g.bounds.UpperBound.Y)*mult))
154 | }
155 | g.ship.ApplyForce(force)
156 |
157 | }
158 |
159 | func (g *Game) Draw(screen *ebiten.Image) {
160 | g.background.Draw(screen, *g.cam)
161 | g.ps.Draw(screen, *g.cam)
162 | g.ship.Draw(screen, *g.cam)
163 |
164 | g.terrain.Draw(screen, *g.cam)
165 | for _, platform := range g.platforms {
166 | platform.Draw(screen, *g.cam)
167 | }
168 | for _, cargo := range g.cargos {
169 | cargo.Draw(screen, *g.cam)
170 | }
171 |
172 | g.drawHood(screen)
173 |
174 | if DrawDebugBodies {
175 | g.drawDebugBodies(screen)
176 | }
177 |
178 | if g.endModal != nil {
179 | g.endModal.Draw(screen)
180 | }
181 |
182 | if PrintDebugInfo {
183 | g.printDebugInfo(screen)
184 | }
185 | }
186 |
187 | func (g *Game) printDebugInfo(screen *ebiten.Image) {
188 | ebitenutil.DebugPrintAt(
189 | screen,
190 | fmt.Sprintf(
191 | "TPS: %0.2f\nFPS: %0.2f\n",
192 | ebiten.CurrentTPS(),
193 | ebiten.CurrentFPS(),
194 | ),
195 | 20, 100)
196 |
197 | }
198 |
199 | func (g *Game) drawDebugBodies(screen *ebiten.Image) {
200 | clr := color.RGBA{
201 | R: 0xff,
202 | G: 0,
203 | B: 0xff,
204 | A: 0xff,
205 | }
206 |
207 | for body := g.world.GetBodyList(); body != nil; body = body.GetNext() {
208 | DrawDebugBody(screen, body, *g.cam, clr)
209 | }
210 | }
211 |
212 | func (g *Game) Layout(w, h int) (int, int) {
213 | return ScreenWidth, ScreenHeight
214 | }
215 |
216 | func (g *Game) drawHood(screen *ebiten.Image) {
217 | screen.DrawImage(hoodImg, nil)
218 |
219 | // Fuel
220 | func() {
221 | opts := &ebiten.DrawImageOptions{}
222 | opts.GeoM.Scale(200, 30)
223 | opts.GeoM.Translate(200, 974)
224 | opts.ColorM.Translate(1, 0, 0, 1)
225 | screen.DrawImage(emptyTransparentImage, opts)
226 |
227 | opts = &ebiten.DrawImageOptions{}
228 | opts.GeoM.Scale(Remap(g.ship.fuel, 0, g.ship.maxFuel, 0, 200), 30)
229 | opts.GeoM.Translate(200, 974)
230 | opts.ColorM.Translate(0, 1, 0, 1)
231 | screen.DrawImage(emptyTransparentImage, opts)
232 | }()
233 |
234 | // Energy
235 | func() {
236 | opts := &ebiten.DrawImageOptions{}
237 | opts.GeoM.Scale(200, 30)
238 | opts.GeoM.Translate(200, 1018)
239 | opts.ColorM.Translate(1, 0, 0, 1)
240 | screen.DrawImage(emptyTransparentImage, opts)
241 |
242 | opts = &ebiten.DrawImageOptions{}
243 | opts.GeoM.Scale(Remap(g.ship.energy, 0, g.ship.maxEnergy, 0, 200), 30)
244 | opts.GeoM.Translate(200, 1018)
245 | opts.ColorM.Translate(0, 1, 0, 1)
246 | screen.DrawImage(emptyTransparentImage, opts)
247 | }()
248 |
249 | g.drawRadar(screen)
250 | }
251 |
252 | // drawRadar draws radar, pointing to current task object
253 | func (g *Game) drawRadar(screen *ebiten.Image) {
254 | if len(g.tasks) == 0 {
255 | return
256 | }
257 |
258 | ang, dist := GetVecsAng(g.ship.GetPos(), g.tasks[0].Pos())
259 | iDist := int(dist)
260 | targetName := g.tasks[0].TargetName()
261 |
262 | if targetName != g.prevTargetName {
263 | g.prevTargetNameImg = ebiten.NewImage(500, 30)
264 | txt := targetName
265 | bounds := text.BoundString(hoodFace, txt)
266 | text.Draw(g.prevTargetNameImg, txt, hoodFace, -bounds.Min.X, -bounds.Min.Y, color.White)
267 | g.prevTargetName = targetName
268 | }
269 | opts := &ebiten.DrawImageOptions{}
270 | opts.GeoM.Translate(750, 974)
271 | screen.DrawImage(g.prevTargetNameImg, opts)
272 |
273 | if iDist != g.prevTargetDistance {
274 | g.prevTargetDistanceImg = ebiten.NewImage(500, 30)
275 | txt := fmt.Sprintf(DistanceText, iDist)
276 | bounds := text.BoundString(hoodFace, txt)
277 | text.Draw(g.prevTargetDistanceImg, txt, hoodFace, -bounds.Min.X, -bounds.Min.Y, color.White)
278 | g.prevTargetDistance = iDist
279 | }
280 | opts = &ebiten.DrawImageOptions{}
281 | opts.GeoM.Translate(750, 1018)
282 | screen.DrawImage(g.prevTargetDistanceImg, opts)
283 |
284 | opts = &ebiten.DrawImageOptions{}
285 | bounds := radarArrowImg.Bounds()
286 | opts.GeoM.Translate(-float64(bounds.Max.X)/2, -float64(bounds.Max.Y)/2)
287 | opts.GeoM.Rotate(ang)
288 | opts.GeoM.Translate(470, 1000)
289 | screen.DrawImage(radarArrowImg, opts)
290 |
291 | }
292 |
293 | func (g *Game) resolveContact(ct ContactType, contact box2d.B2ContactInterface, impulse *box2d.B2ContactImpulse) {
294 | if ct == ContactTypeBegin && !contact.IsTouching() {
295 | return
296 | }
297 |
298 | a := contact.GetFixtureA().GetBody().GetUserData()
299 | b := contact.GetFixtureB().GetBody().GetUserData()
300 |
301 | if part, ok := a.(Part); ok {
302 | g.ShipPartContact(ct, contact, impulse, part, b)
303 | return
304 | }
305 | if part, ok := b.(Part); ok {
306 | g.ShipPartContact(ct, contact, impulse, part, a)
307 | return
308 | }
309 |
310 | if cargo, ok := a.(*Cargo); ok {
311 | g.CargoContact(ct, contact, impulse, cargo, b)
312 | return
313 | }
314 | if cargo, ok := b.(*Cargo); ok {
315 | g.CargoContact(ct, contact, impulse, cargo, a)
316 | return
317 | }
318 | }
319 |
320 | func (g *Game) BeginContact(contact box2d.B2ContactInterface) {
321 | g.resolveContact(ContactTypeBegin, contact, nil)
322 | }
323 |
324 | func (g *Game) EndContact(contact box2d.B2ContactInterface) {
325 | g.resolveContact(ContactTypeEnd, contact, nil)
326 | }
327 |
328 | func (g *Game) PreSolve(contact box2d.B2ContactInterface, oldManifold box2d.B2Manifold) {
329 | g.resolveContact(ContactTypePreSolve, contact, nil)
330 | }
331 |
332 | func (g *Game) PostSolve(contact box2d.B2ContactInterface, impulse *box2d.B2ContactImpulse) {
333 | g.resolveContact(ContactTypePostSolve, contact, impulse)
334 | }
335 |
336 | func (g *Game) ShipPartContact(
337 | ct ContactType,
338 | contact box2d.B2ContactInterface,
339 | impulse *box2d.B2ContactImpulse,
340 | part Part,
341 | other interface{}) {
342 |
343 | if ct == ContactTypePostSolve {
344 | imp := impulse.NormalImpulses[0]
345 | if imp > ShipImpulseDestructionThreshold {
346 | g.ship.energy -= imp
347 | if g.ship.energy < 0 {
348 | g.ship.energy = 0
349 | }
350 | }
351 | }
352 |
353 | switch obj := other.(type) {
354 | case *Platform:
355 | g.ShipPlatformContact(ct, obj)
356 | default:
357 | //fmt.Printf("unknown body %v\n", obj)
358 | }
359 | }
360 |
361 | func (g *Game) ShipPlatformContact(
362 | ct ContactType,
363 | platform *Platform) {
364 |
365 | switch ct {
366 | case ContactTypeBegin:
367 | g.ship.contactPlatform = platform
368 | platform.ship = g.ship
369 |
370 | case ContactTypeEnd:
371 | g.ship.contactPlatform = nil
372 | platform.ship = nil
373 | }
374 | }
375 |
376 | // ----------------------
377 |
378 | func (g *Game) CargoContact(
379 | ct ContactType,
380 | contact box2d.B2ContactInterface,
381 | impulse *box2d.B2ContactImpulse,
382 | cargo *Cargo,
383 | other interface{}) {
384 |
385 | switch obj := other.(type) {
386 | case *Platform:
387 | g.CargoPlatformContact(ct, cargo, obj)
388 | default:
389 | //fmt.Printf("unknown body %v\n", obj)
390 | }
391 | }
392 |
393 | func (g *Game) CargoPlatformContact(
394 | ct ContactType,
395 | cargo *Cargo,
396 | platform *Platform) {
397 |
398 | switch ct {
399 | case ContactTypeBegin:
400 | cargo.platform = platform
401 | case ContactTypeEnd:
402 | cargo.platform = nil
403 | }
404 | }
405 |
406 | // ----------------------
407 |
408 | type ContactType string
409 |
410 | const (
411 | ContactTypeBegin = "BeginContact"
412 | ContactTypeEnd = "EndContact"
413 | ContactTypePreSolve = "PreSolve"
414 | ContactTypePostSolve = "PostSolve"
415 | )
416 |
--------------------------------------------------------------------------------