├── 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 | 16 | 37 | 39 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /assets/parts/cargo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 41 | 45 | 46 | 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 | 16 | 37 | 39 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/parts/leg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/parts/engine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/parts/leg_fastening.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/parts/cabin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 43 | 47 | 48 | 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 | 16 | 37 | 39 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Space Crane game 2 | ================ 3 | 4 | 5 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/spiritofsim/go-space-crane) 6 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/spiritofsim/go-space-crane/Go) 7 | [![codecov](https://codecov.io/gh/spiritofsim/go-space-crane/branch/master/graph/badge.svg)](https://codecov.io/gh/spiritofsim/go-space-crane) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/spiritofsim/fns)](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 | ![img_1.png](screens/1.png) 18 | 19 | ## Features 20 | ### Levels 21 | - Levels are simple svg's you can draw in any SVG editor 22 | ![img_1.png](screens/level.png) 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 | 16 | 37 | 39 | 41 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/parts/crane_upper_jaw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 41 | 44 | 47 | 48 | 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 | 13 | 17 | 18 | title 19 | desc 20 | 21 | 22 | desc 23 | title 24 | 25 | 31 | 32 | ` 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 | 16 | 35 | 37 | 41 | 48 | 10000 50 | platform 52 | 53 | 60 | 10000 62 | platform 64 | 65 | 72 | ship 74 | 75 | 82 | cargo 84 | 85 | 92 | bounds 94 | 95 | 99 | 100 | 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 | 16 | 35 | 37 | 41 | 48 | 10000 50 | platform 52 | 53 | 60 | ship 62 | 63 | 70 | bounds 72 | 73 | 77 | 84 | 10000 86 | platform 88 | 89 | 96 | 10000 98 | platform 100 | 101 | 108 | 10000 110 | platform 112 | 113 | 120 | 10000 122 | platform 124 | 125 | 132 | 10000 134 | platform 136 | 137 | 144 | 10000 146 | platform 148 | 149 | 156 | 10000 158 | platform 160 | 161 | 168 | 10000 170 | platform 172 | 173 | 174 | 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 | --------------------------------------------------------------------------------