diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b7467b7..9a9bea9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,9 +21,9 @@ jobs: uses: actions/checkout@v2 - name: Setup Go - uses: actions/setup-go@v2.1.3 + uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.19 - name: Install Linux Packages if: matrix.os == 'ubuntu-latest' diff --git a/.gitignore b/.gitignore index c18817e..4298873 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ # Distribution /dist + +# Go +trace.out diff --git a/README.md b/README.md index 758f8c6..db662ee 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Rally MKA [![Go Report Card](https://goreportcard.com/badge/github.com/mokiat/rally-mka)](https://goreportcard.com/report/github.com/mokiat/rally-mka) [![Go Reference](https://pkg.go.dev/badge/github.com/mokiat/rally-mka@master.svg)](https://pkg.go.dev/github.com/mokiat/rally-mka@master) -Rally MKA is a really old game/demo of mine ported to Go for fun and as a way to experiment with new concepts. +Rally MKA is a really old game/demo of mine ported to Go for fun and as a way to experiment with new concepts. It is also a showcase for the [lacking](https://github.com/mokiat/lacking) game engine. + +[![Game Screenshot](preview.png)](https://mokiat.itch.io/rally-mka) -[![Game Screenshot](preview.png)](http://mokiat.com/rally-mka/) ## User's Guide ### Browser -Use the following link to try the game in the browser: -http://mokiat.com/rally-mka/ +You can play the game on [itch.io](https://mokiat.itch.io/rally-mka). The preferred browser is [Chrome](https://www.google.com/chrome/), which at the time of writing appears to best support WebGL2, WebAssembly and Game Controllers. @@ -19,27 +19,13 @@ Check the [Releases](https://github.com/mokiat/rally-mka/releases) section for r The requirement is that your OS supports `OpenGL 4.6`. -### Controls - -Use the following keyboard keys when playing: - -- `Left Arrow` - Steer left -- `Right Arrow` - Steer right -- `Up Arrow` - Accelerate -- `Down Arrow` - Decelerate -- `a`, `s`, `d`, `w` - Rotate camera -- `q`, `e` - Zoom in/out camera -- `Enter` - Flip car - -In addition, the game supports using a Game Controller. - ## Developer's Guide This section describes how to setup the project on your machine and compile it yourself. ### Prerequisites -- You need [Go 1.18](https://golang.org/dl/) or newer. +- You need [Go 1.20](https://golang.org/dl/) or newer. - You need the [Git LFS](https://git-lfs.github.com/) plugin. As the project contains large images and models, this is the official way on how not to clog a repository. - Follow the instructions on the [GLFW for Go](https://github.com/go-gl/glfw) repository and make sure you can run the [GLFW examples](https://github.com/go-gl/example) on your platform. - Make sure you have [Task](https://taskfile.dev/) installed, as this project uses Taskfiles. @@ -87,7 +73,7 @@ This section describes how to setup the project on your machine and compile it y task wasm ``` -1. Run HTTP server +1. Run an HTTP server ```sh task web @@ -104,15 +90,3 @@ All source code in this project is licensed under [Apache License v2](LICENSE). ### Assets Assets (images, models, textures, etc.) are distributed under the [Creative Commons Attribution 4.0 International](http://creativecommons.org/licenses/by/4.0/) license. - -## Special Thanks - -The following projects and individuals have contributed significantly to the project: - -- **[The Go Team](https://go.dev/)** for making Go programming language. -- **[GLFW for Go](https://github.com/go-gl/glfw)** for making it possible to use GLFW and OpenGL in Go. -- **[LearnOpenGL](https://learnopengl.com/)** for the amazing tutorials. -- **[Poly Haven](https://polyhaven.com/)** for the excellent free images. -- **[Erin Catto](https://github.com/erincatto)** for all the presentations and articles that were used as reference for the physics engine. -- **[Bo0mer](https://github.com/Bo0mer)** for the panoramic image that was used to generate the original `city` skybox images. -- And everyone else whose repository has been used as a dependency. diff --git a/Taskfile.yml b/Taskfile.yml index d004119..1156f0c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,21 +2,25 @@ version: '3' tasks: + licenses: + cmds: + - go run github.com/google/go-licenses@v1.6.0 report --template ./resources/licenses.tmpl ./cmd/rallymka ./cmd/rallypack | fold -w 80 -s > ./resources/licenses.txt + + pack: + cmds: + - go run './cmd/rallypack' + webpack: cmds: - mkdir -p 'assets/web' - - cp 'resources/icon.png' 'assets/web/favicon.png' + - cp 'resources/ui/images/icon.png' 'assets/web/favicon.png' - cp 'resources/web/main.css' 'assets/web/main.css' - cp 'resources/web/main.js' 'assets/web/main.js' - cp 'resources/web/index.html' 'assets/index.html' - cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" 'assets/web' - pack: - cmds: - - go run './cmd/rallypack' - assets: - deps: [webpack, pack] + deps: [pack, webpack] run: cmds: @@ -31,8 +35,29 @@ tasks: GOOS: js GOARCH: wasm cmds: - - go build -o './assets/web/main.wasm' './cmd/rallywasm' + - go build -o './assets/web/main.wasm' './cmd/rallymka' web: cmds: - - go run 'github.com/mokiat/httpserv@master' -dir './assets' -host '127.0.0.1' + - go run 'github.com/mokiat/httpserv@v1.0.0' -dir './assets' -host '127.0.0.1' + + profile-cpu: + cmds: + - go tool pprof 'http://localhost:6060/debug/pprof/profile?seconds=30' + + profile-alloc: + cmds: + - go tool pprof 'http://localhost:6060/debug/pprof/allocs?seconds=30' + + profile-heap: + cmds: + - go tool pprof 'http://localhost:6060/debug/pprof/heap?seconds=30' + + profile-snap: + cmds: + - go tool pprof 'http://localhost:6060/debug/pprof/heap' + + trace: + cmds: + - curl -o trace.out 'http://localhost:6060/debug/pprof/trace?seconds=30' + - go tool trace trace.out diff --git a/cmd/rallymka/app_cgo.go b/cmd/rallymka/app_cgo.go new file mode 100644 index 0000000..a925b6e --- /dev/null +++ b/cmd/rallymka/app_cgo.go @@ -0,0 +1,43 @@ +//go:build !js + +package main + +import ( + "fmt" + + glapp "github.com/mokiat/lacking-gl/app" + glgame "github.com/mokiat/lacking-gl/game" + glrender "github.com/mokiat/lacking-gl/render" + glui "github.com/mokiat/lacking-gl/ui" + "github.com/mokiat/lacking/app" + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/game/asset" + "github.com/mokiat/lacking/ui" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/util/resource" + gameui "github.com/mokiat/rally-mka/internal/ui" +) + +func runApplication() error { + registry, err := asset.NewDirRegistry(".") + if err != nil { + return fmt.Errorf("failed to initialize registry: %w", err) + } + + renderAPI := glrender.NewAPI() + gameController := game.NewController(registry, renderAPI, glgame.NewShaderCollection()) + resourceLocator := mat.WrappedResourceLocator(resource.NewFileLocator("./resources")) + uiCfg := ui.NewConfig(resourceLocator, renderAPI, glui.NewShaderCollection()) + uiController := ui.NewController(uiCfg, func(w *ui.Window) { + gameui.BootstrapApplication(w, gameController) + }) + + cfg := glapp.NewConfig("Rally MKA", 1024, 576) + cfg.SetFullscreen(true) + cfg.SetMaximized(false) + cfg.SetMinSize(1024, 576) + cfg.SetVSync(true) + cfg.SetIcon("resources/ui/images/icon.png") + cfg.SetMaximized(true) + return glapp.Run(cfg, app.NewLayeredController(gameController, uiController)) +} diff --git a/cmd/rallywasm/main.go b/cmd/rallymka/app_js.go similarity index 67% rename from cmd/rallywasm/main.go rename to cmd/rallymka/app_js.go index 5ade053..8abf42c 100644 --- a/cmd/rallywasm/main.go +++ b/cmd/rallymka/app_js.go @@ -1,36 +1,24 @@ -//go:build js && wasm +//go:build js package main import ( "fmt" - "os" jsapp "github.com/mokiat/lacking-js/app" jsgame "github.com/mokiat/lacking-js/game" jsrender "github.com/mokiat/lacking-js/render" jsui "github.com/mokiat/lacking-js/ui" "github.com/mokiat/lacking/app" + "github.com/mokiat/lacking/game" "github.com/mokiat/lacking/game/asset" - "github.com/mokiat/lacking/game/graphics" - "github.com/mokiat/lacking/log" "github.com/mokiat/lacking/ui" "github.com/mokiat/lacking/ui/mat" "github.com/mokiat/lacking/util/resource" - "github.com/mokiat/rally-mka/internal/game" gameui "github.com/mokiat/rally-mka/internal/ui" "github.com/mokiat/rally-mka/resources" ) -func main() { - log.Info("Started") - if err := runApplication(); err != nil { - log.Error("Crashed: %v", err) - os.Exit(1) - } - log.Info("Stopped") -} - func runApplication() error { registry, err := asset.NewWebRegistry(".") if err != nil { @@ -38,8 +26,7 @@ func runApplication() error { } resourceLocator := mat.WrappedResourceLocator(resource.NewFSLocator(resources.UI)) renderAPI := jsrender.NewAPI() - graphicsEngine := graphics.NewEngine(renderAPI, jsgame.NewShaderCollection()) - gameController := game.NewController(registry, graphicsEngine) + gameController := game.NewController(registry, renderAPI, jsgame.NewShaderCollection()) uiCfg := ui.NewConfig(resourceLocator, renderAPI, jsui.NewShaderCollection()) uiController := ui.NewController(uiCfg, func(w *ui.Window) { gameui.BootstrapApplication(w, gameController) @@ -47,10 +34,6 @@ func runApplication() error { cfg := jsapp.NewConfig("screen") cfg.AddGLExtension("EXT_color_buffer_float") - - controller := app.NewLayeredController( - gameController, - uiController, - ) - return jsapp.Run(cfg, controller) + cfg.SetFullscreen(false) + return jsapp.Run(cfg, app.NewLayeredController(gameController, uiController)) } diff --git a/cmd/rallymka/main.go b/cmd/rallymka/main.go index 72d77fa..d1d18f7 100644 --- a/cmd/rallymka/main.go +++ b/cmd/rallymka/main.go @@ -1,22 +1,9 @@ package main import ( - "fmt" "os" - glapp "github.com/mokiat/lacking-gl/app" - glgame "github.com/mokiat/lacking-gl/game" - glrender "github.com/mokiat/lacking-gl/render" - glui "github.com/mokiat/lacking-gl/ui" - "github.com/mokiat/lacking/app" - "github.com/mokiat/lacking/game/asset" - "github.com/mokiat/lacking/game/graphics" "github.com/mokiat/lacking/log" - "github.com/mokiat/lacking/ui" - "github.com/mokiat/lacking/ui/mat" - "github.com/mokiat/lacking/util/resource" - "github.com/mokiat/rally-mka/internal/game" - gameui "github.com/mokiat/rally-mka/internal/ui" ) func main() { @@ -27,29 +14,3 @@ func main() { } log.Info("Stopped") } - -func runApplication() error { - cfg := glapp.NewConfig("Rally MKA", 1024, 576) - cfg.SetVSync(true) - cfg.SetIcon("resources/icon.png") - - registry, err := asset.NewDirRegistry(".") - if err != nil { - return fmt.Errorf("failed to initialize registry: %w", err) - } - - renderAPI := glrender.NewAPI() - graphicsEngine := graphics.NewEngine(renderAPI, glgame.NewShaderCollection()) - gameController := game.NewController(registry, graphicsEngine) - resourceLocator := mat.WrappedResourceLocator(resource.NewFileLocator("./resources")) - uiCfg := ui.NewConfig(resourceLocator, renderAPI, glui.NewShaderCollection()) - uiController := ui.NewController(uiCfg, func(w *ui.Window) { - gameui.BootstrapApplication(w, gameController) - }) - - controller := app.NewLayeredController( - gameController, - uiController, - ) - return glapp.Run(cfg, controller) -} diff --git a/cmd/rallypack/main.go b/cmd/rallypack/main.go index f8897b8..0858e97 100644 --- a/cmd/rallypack/main.go +++ b/cmd/rallypack/main.go @@ -5,7 +5,7 @@ import ( "log" "github.com/mokiat/lacking/data/pack" - gameasset "github.com/mokiat/lacking/game/asset" + "github.com/mokiat/lacking/game/asset" ) func main() { @@ -15,67 +15,39 @@ func main() { } func runTool() error { - registry, err := gameasset.NewDirRegistry(".") + registry, err := asset.NewDirRegistry(".") if err != nil { return fmt.Errorf("failed to create registry: %w", err) } - var ( - tex2DConcrete = ensureResource(registry, "e89d3c68-12ba-42ca-bc04-ccefefcf5720", "twod_texture", "Concrete") - tex2DRoad = ensureResource(registry, "b2c7a46f-f2a2-4601-bd10-493a68fc094c", "twod_texture", "Road") - tex2DBarrier = ensureResource(registry, "3800657a-4407-4bda-bdff-b57748c002ab", "twod_texture", "Barrier") - tex2DGrass = ensureResource(registry, "5905593c-6820-465b-8315-1e17be0a6f72", "twod_texture", "Grass") - tex2DGravel = ensureResource(registry, "fee51386-e8f1-4dd7-926e-3ca353589e01", "twod_texture", "Gravel") - tex2DRustyMetal = ensureResource(registry, "7574ab74-980b-4bbd-aea1-3459998ccc71", "twod_texture", "Rusty Metal") - tex2DCarBody = ensureResource(registry, "8ed7f73b-0129-48f8-b187-b7e43eff0294", "twod_texture", "Car Body") - tex2DCarWheel = ensureResource(registry, "9d52b91b-0292-4442-b100-29f12402c2c1", "twod_texture", "Car Wheel") - tex2DLeafyTree = ensureResource(registry, "01b8ab21-3d72-4b31-869e-37039dee0161", "twod_texture", "Leafy Tree") - ) - - var ( - texCubeSkybox = ensureResource(registry, "bab99e80-ded1-459a-b00b-6a17afa44046", "cube_texture", "Skybox") - texCubeSkyboxReflection = ensureResource(registry, "eb639f55-d6eb-46d7-bd3b-d52fcaa0bc58", "cube_texture", "Skybox Reflection") - texCubeSkyboxRefraction = ensureResource(registry, "0815fb89-7ae6-4229-b9e2-59610c4fc6bc", "cube_texture", "Skybox Refraction") - ) - - var ( - modelStreetLamp = ensureResource(registry, "31cb3900-760d-4179-b5d9-79f8e69be8f6", "model", "Street Lamp") - modelSUV = ensureResource(registry, "eaeb7483-7271-441f-a470-c0a8fa225161", "model", "SUV") - modelLeafyTree = ensureResource(registry, "2c6e3211-68f8-4b31-beaf-e52af5d3be31", "model", "Leafy Tree") - ) - - modelStreetLamp.AddDependency(tex2DRustyMetal) - modelSUV.AddDependency(tex2DCarBody) - modelSUV.AddDependency(tex2DCarWheel) - modelLeafyTree.AddDependency(tex2DLeafyTree) - - var ( - levelPlayground = ensureResource(registry, "9ca25b5c-ffa0-4224-ad80-a3c4d67930b7", "scene", "Playground") - levelForest = ensureResource(registry, "884e6395-2300-47bb-9916-b80e3dc0e086", "scene", "Forest") - levelHighway = ensureResource(registry, "acf21108-47ad-44ef-ba21-bf5473bfbaa0", "scene", "Highway") - ) - - levelPlayground.AddDependency(tex2DGrass) - levelPlayground.AddDependency(texCubeSkybox) - levelPlayground.AddDependency(texCubeSkyboxReflection) - levelPlayground.AddDependency(texCubeSkyboxRefraction) - levelPlayground.AddDependency(modelSUV) - levelPlayground.AddDependency(modelLeafyTree) - levelForest.AddDependency(tex2DGravel) - levelForest.AddDependency(tex2DGrass) - levelForest.AddDependency(tex2DBarrier) - levelForest.AddDependency(texCubeSkybox) - levelForest.AddDependency(texCubeSkyboxReflection) - levelForest.AddDependency(texCubeSkyboxRefraction) - levelForest.AddDependency(modelSUV) - levelForest.AddDependency(modelLeafyTree) - levelHighway.AddDependency(tex2DRoad) - levelHighway.AddDependency(tex2DConcrete) - levelHighway.AddDependency(texCubeSkybox) - levelHighway.AddDependency(texCubeSkyboxReflection) - levelHighway.AddDependency(texCubeSkyboxRefraction) - levelHighway.AddDependency(modelSUV) - levelHighway.AddDependency(modelStreetLamp) + skyboxDay := ensureResource(registry, "bab99e80-ded1-459a-b00b-6a17afa44046", "cube_texture", "Skybox Day") + skyboxDayReflection := ensureResource(registry, "eb639f55-d6eb-46d7-bd3b-d52fcaa0bc58", "cube_texture", "Skybox Day Reflection") + skyboxDayRefraction := ensureResource(registry, "0815fb89-7ae6-4229-b9e2-59610c4fc6bc", "cube_texture", "Skybox Day Refraction") + + skyboxNight := ensureResource(registry, "904431e9-7d78-4cbb-ae46-2e70b1458832", "cube_texture", "Skybox Night") + skyboxNightReflection := ensureResource(registry, "892ece47-c4c8-4ff0-a9ad-0faa208eee81", "cube_texture", "Skybox Night Reflection") + skyboxNightRefraction := ensureResource(registry, "e79da33a-5131-4029-b168-ea2c5378c169", "cube_texture", "Skybox Night Refraction") + + modelHomeScreen := ensureResource(registry, "d1aef712-4c5a-45b8-ba6f-0385e071a8f1", "model", "Content: Home Screen") + modelForest := ensureResource(registry, "5f7bd967-dc4a-4252-b1a5-5721cd299d67", "model", "Forest Ride") + modelSUV := ensureResource(registry, "eaeb7483-7271-441f-a470-c0a8fa225161", "model", "SUV") + + levelHomeScreen := ensureResource(registry, "80dd9049-c183-4d82-b5b2-6f38ca499055", "scene", "Home Screen") + levelHomeScreen.AddDependency(modelHomeScreen) + + levelForestDay := ensureResource(registry, "884e6395-2300-47bb-9916-b80e3dc0e086", "scene", "Forest-Day") + levelForestDay.AddDependency(skyboxDay) + levelForestDay.AddDependency(skyboxDayReflection) + levelForestDay.AddDependency(skyboxDayRefraction) + levelForestDay.AddDependency(modelForest) + levelForestDay.AddDependency(modelSUV) + + levelForestNight := ensureResource(registry, "a288e44d-3ed5-415b-b9c2-4dbe086dfce2", "scene", "Forest-Night") + levelForestNight.AddDependency(skyboxNight) + levelForestNight.AddDependency(skyboxNightReflection) + levelForestNight.AddDependency(skyboxNightRefraction) + levelForestNight.AddDependency(modelForest) + levelForestNight.AddDependency(modelSUV) if err := registry.Save(); err != nil { return fmt.Errorf("error saving resources: %w", err) @@ -83,112 +55,117 @@ func runTool() error { packer := pack.NewPacker(registry) - // TwoD Textures + // Cube Textures packer.Pipeline(func(p *pack.Pipeline) { - p.SaveTwoDTextureAsset(tex2DConcrete.ID(), - p.OpenImageResource("resources/images/concrete.png"), - ) - - p.SaveTwoDTextureAsset(tex2DRoad.ID(), - p.OpenImageResource("resources/images/road.png"), - ) - - p.SaveTwoDTextureAsset(tex2DBarrier.ID(), - p.OpenImageResource("resources/images/barrier.png"), - ) - - p.SaveTwoDTextureAsset(tex2DGrass.ID(), - p.OpenImageResource("resources/images/grass.png"), - ) - - p.SaveTwoDTextureAsset(tex2DGravel.ID(), - p.OpenImageResource("resources/images/gravel.png"), + equirectangularImage := p.OpenImageResource("resources/images/day.hdr") + cubeImage := p.BuildCubeImage( + pack.WithFrontImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideFront, equirectangularImage)), + pack.WithRearImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideRear, equirectangularImage)), + pack.WithLeftImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideLeft, equirectangularImage)), + pack.WithRightImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideRight, equirectangularImage)), + pack.WithTopImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideTop, equirectangularImage)), + pack.WithBottomImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideBottom, equirectangularImage)), ) - p.SaveTwoDTextureAsset(tex2DRustyMetal.ID(), - p.OpenImageResource("resources/images/rusty_metal_02_diff_512.png"), + smallerCubeImage := p.ScaleCubeImage(cubeImage, 512) + p.SaveCubeTextureAsset(skyboxDay, smallerCubeImage, + pack.WithFormat(asset.TexelFormatRGBA16F), ) - p.SaveTwoDTextureAsset(tex2DCarBody.ID(), - p.OpenImageResource("resources/images/body.png"), + reflectionCubeImage := p.ScaleCubeImage(cubeImage, 128) + p.SaveCubeTextureAsset(skyboxDayReflection, reflectionCubeImage, + pack.WithFormat(asset.TexelFormatRGBA16F), ) - p.SaveTwoDTextureAsset(tex2DCarWheel.ID(), - p.OpenImageResource("resources/images/wheel.png"), + refractionCubeImage := p.BuildIrradianceCubeImage(reflectionCubeImage, + pack.WithSampleCount(50), ) - - p.SaveTwoDTextureAsset(tex2DLeafyTree.ID(), - p.OpenImageResource("resources/images/leafy_tree.png"), + p.SaveCubeTextureAsset(skyboxDayRefraction, refractionCubeImage, + pack.WithFormat(asset.TexelFormatRGBA16F), ) }) - // Cube Textures packer.Pipeline(func(p *pack.Pipeline) { - srcEquirectangularImage := p.OpenImageResource("resources/images/syferfontein.hdr") - skyboxCubeImage := p.BuildCubeImage( - pack.WithFrontImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideFront, srcEquirectangularImage)), - pack.WithRearImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideRear, srcEquirectangularImage)), - pack.WithLeftImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideLeft, srcEquirectangularImage)), - pack.WithRightImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideRight, srcEquirectangularImage)), - pack.WithTopImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideTop, srcEquirectangularImage)), - pack.WithBottomImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideBottom, srcEquirectangularImage)), + // Using GIMP: + // 1. Scale height to %50 + // 2. Convert to float32 + // 3. Exposure: ~ -6 + + equirectangularImage := p.OpenImageResource("resources/images/night.exr") + cubeImage := p.BuildCubeImage( + pack.WithFrontImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideFront, equirectangularImage)), + pack.WithRearImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideRear, equirectangularImage)), + pack.WithLeftImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideLeft, equirectangularImage)), + pack.WithRightImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideRight, equirectangularImage)), + pack.WithTopImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideTop, equirectangularImage)), + pack.WithBottomImage(p.BuildCubeSideFromEquirectangular(pack.CubeSideBottom, equirectangularImage)), ) - smallerSkyboxCubeImage := p.ScaleCubeImage(skyboxCubeImage, 512) - p.SaveCubeTextureAsset(texCubeSkybox.ID(), smallerSkyboxCubeImage, - pack.WithFormat(gameasset.TexelFormatRGBA16F), + smallerCubeImage := p.ScaleCubeImage(cubeImage, 512) + p.SaveCubeTextureAsset(skyboxNight, smallerCubeImage, + pack.WithFormat(asset.TexelFormatRGBA16F), ) - reflectionCubeImage := p.ScaleCubeImage(skyboxCubeImage, 128) - p.SaveCubeTextureAsset(texCubeSkyboxReflection.ID(), reflectionCubeImage, - pack.WithFormat(gameasset.TexelFormatRGBA16F), + reflectionCubeImage := p.ScaleCubeImage(cubeImage, 128) + p.SaveCubeTextureAsset(skyboxNightReflection, reflectionCubeImage, + pack.WithFormat(asset.TexelFormatRGBA16F), ) refractionCubeImage := p.BuildIrradianceCubeImage(reflectionCubeImage, pack.WithSampleCount(50), ) - p.SaveCubeTextureAsset(texCubeSkyboxRefraction.ID(), refractionCubeImage, - pack.WithFormat(gameasset.TexelFormatRGBA16F), + p.SaveCubeTextureAsset(skyboxNightRefraction, refractionCubeImage, + pack.WithFormat(asset.TexelFormatRGBA16F), ) }) // Models packer.Pipeline(func(p *pack.Pipeline) { - p.SaveModelAsset(modelStreetLamp.ID(), - p.OpenGLTFResource("resources/models/street_lamp.gltf"), + p.SaveModelAsset(modelHomeScreen, + p.OpenGLTFResource("resources/models/home-screen.glb"), ) + }) - p.SaveModelAsset(modelSUV.ID(), - p.OpenGLTFResource("resources/models/suv.gltf"), + packer.Pipeline(func(p *pack.Pipeline) { + p.SaveModelAsset(modelSUV, + p.OpenGLTFResource("resources/models/suv.glb"), ) + }) - p.SaveModelAsset(modelLeafyTree.ID(), - p.OpenGLTFResource("resources/models/leafy_tree.gltf"), + packer.Pipeline(func(p *pack.Pipeline) { + p.SaveModelAsset(modelForest, + p.OpenGLTFResource("resources/models/forest.glb"), ) }) // Levels packer.Pipeline(func(p *pack.Pipeline) { - p.SaveLevelAsset(levelPlayground.ID(), - p.OpenLevelResource("resources/levels/playground.json"), + p.SaveLevelAsset(levelHomeScreen, + p.OpenLevelResource("resources/levels/home-screen.json"), ) + }) - p.SaveLevelAsset(levelForest.ID(), - p.OpenLevelResource("resources/levels/forest.json"), + packer.Pipeline(func(p *pack.Pipeline) { + p.SaveLevelAsset(levelForestDay, + p.OpenLevelResource("resources/levels/forest-day.json"), ) + }) - p.SaveLevelAsset(levelHighway.ID(), - p.OpenLevelResource("resources/levels/highway.json"), + packer.Pipeline(func(p *pack.Pipeline) { + p.SaveLevelAsset(levelForestNight, + p.OpenLevelResource("resources/levels/forest-night.json"), ) }) return packer.RunParallel() } -func ensureResource(registry gameasset.Registry, id, kind, name string) gameasset.Resource { +func ensureResource(registry asset.Registry, id, kind, name string) asset.Resource { resource := registry.ResourceByID(id) if resource == nil { resource = registry.CreateIDResource(id, kind, name) + } else { + resource.SetName(name) } return resource } diff --git a/go.mod b/go.mod index bf4bb27..5b6705c 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,36 @@ module github.com/mokiat/rally-mka -go 1.18 +go 1.20 require ( - github.com/mokiat/gomath v0.2.0 - github.com/mokiat/lacking v0.8.0 - github.com/mokiat/lacking-gl v0.5.0 - github.com/mokiat/lacking-js v0.5.0 + github.com/mokiat/gblob v0.2.1 + github.com/mokiat/gog v0.8.0 + github.com/mokiat/gomath v0.7.0 + github.com/mokiat/lacking v0.10.0 + github.com/mokiat/lacking-gl v0.10.0 + github.com/mokiat/lacking-js v0.10.0 + github.com/x448/float16 v0.8.4 + golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea ) require ( github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220516021902-eb3e265c7661 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mdouchement/hdr v0.2.3 // indirect + github.com/mdouchement/hdr v0.2.4 // indirect github.com/mokiat/goexr v0.1.0 // indirect - github.com/mokiat/wasmgl v0.4.0 // indirect - github.com/qmuntal/gltf v0.22.0 // indirect - github.com/x448/float16 v0.8.4 // indirect - golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect - golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect - golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect - golang.org/x/text v0.3.7 // indirect + github.com/mokiat/wasmgl v0.5.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/qmuntal/gltf v0.23.1 // indirect + golang.org/x/image v0.7.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.9.1 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 2997c2d..8f99383 100644 --- a/go.sum +++ b/go.sum @@ -1,78 +1,96 @@ -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220516021902-eb3e265c7661 h1:1bpooddSK2996NWM/1TW59cchQOm9MkoV9DkhSJH1BI= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220516021902-eb3e265c7661/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/pprof v0.0.0-20230222194610-99052d3372e7 h1:pNFnpaSXfibgW7aUbk9pwLmI7LNwh/iR46x/YwN/lNg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mdouchement/hdr v0.2.3 h1:IxzxLgEarv9VPsk17DGIWnhA60MA5ASbekCU8YSFmaY= -github.com/mdouchement/hdr v0.2.3/go.mod h1:2Yb3JAST9c66wTLZ1vQrejU5sDeOC+ukXVs1pFAUtPo= +github.com/mdouchement/hdr v0.2.4 h1:k0ojx7smWvWw8En2BjUnb144j48gAExu5mv+ogNrkTc= +github.com/mdouchement/hdr v0.2.4/go.mod h1:uezK2oUhYtoRLkTD0J4ryiOsu/oWLjRXx0I/92mIRmQ= +github.com/mokiat/gblob v0.2.1 h1:PIHED13GCWOkTle3v1peOPuoaOvXZSyoWqnJ95fXsEo= +github.com/mokiat/gblob v0.2.1/go.mod h1:nGZoWg6U2bJQUub7oaSUEV0obD3rtV+PLTftVFBzSxE= github.com/mokiat/goexr v0.1.0 h1:zoDvzvIjs/GpkxJDVcCP6GafLp1nOuNDef9JL8KSd2A= github.com/mokiat/goexr v0.1.0/go.mod h1:KhERYaXCcY2ZEaTg1/LyzJ7pxdj/q3V1TxgViG86ck4= -github.com/mokiat/gomath v0.2.0 h1:ymEIRySh5aVHvqNJfnqJ1aiFNBHzUGy9801oJRLb5mk= -github.com/mokiat/gomath v0.2.0/go.mod h1:HzUDh5PiQcvnXRAwMmdNOqUO3h6IdouLd2BhikJIXz4= -github.com/mokiat/lacking v0.8.0 h1:f6ZwBOxYSvuVFc4d8DeNgY2zQR+smi24l3G/QPQTcPk= -github.com/mokiat/lacking v0.8.0/go.mod h1:MM1AX7zzUK+fHNRc08auxBVVL7/R9cfVKfwl9TGYXug= -github.com/mokiat/lacking-gl v0.5.0 h1:Of76VcoiBSYx3S5bn6R4i4VcKeXFZ+5wrfWMUc/x2io= -github.com/mokiat/lacking-gl v0.5.0/go.mod h1:rFaJXy0VdZSsCnma8sFg/f4KxYfmHdBHFiEqHu2Xh08= -github.com/mokiat/lacking-js v0.5.0 h1:WpLix0sJ50x8n3dIIbvDpd5/QP3prwJ7ypo2bufyK7Y= -github.com/mokiat/lacking-js v0.5.0/go.mod h1:XsGZjWrmfEGgpEt5MdnhA88Wu7vzCQmgXvLAkGKzKJ8= -github.com/mokiat/wasmgl v0.4.0 h1:nnVWHibX8VoGXKlDHlwgdD/JmCHSasRQGmQfUlIEnbY= -github.com/mokiat/wasmgl v0.4.0/go.mod h1:Ikx+bDVpuq+OkvdDDwKI0pIluqK+kduVHJhRFBmmGrw= -github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/qmuntal/gltf v0.22.0 h1:IwoanftOUqj0X0KECjBN5+dm0SR9s9TDEevgpeej36U= -github.com/qmuntal/gltf v0.22.0/go.mod h1:7FR0CRHoOehIgKTBVq/QVyvPn0i6tzp2AdIghb2bPg4= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/mokiat/gog v0.8.0 h1:crjAWA3rIdpAuNVwqCV/CG6RiwKQ2Wn0gh17YuXeCGs= +github.com/mokiat/gog v0.8.0/go.mod h1:xWKrZVIvgDGMou+FwFvcJctdjLSwttBPYnwTiJpycOg= +github.com/mokiat/gomath v0.7.0 h1:fcrjgVeL7S2i4aBEzS5XLhCNh5IRqrYchEBr/d6YI2U= +github.com/mokiat/gomath v0.7.0/go.mod h1:bf4K7x67Otao/aWBAqN8/K7j90ZVD4pq9UaBUjvZbNk= +github.com/mokiat/lacking v0.10.0 h1:yARKbjoXSZaTwpQyC6Kwi22otiq+ieprJb2cf2Bo7v8= +github.com/mokiat/lacking v0.10.0/go.mod h1:uuFO8vH6QjIw6+qOoHfksYN17n7AEQY3UNcLs9Kw5Go= +github.com/mokiat/lacking-gl v0.10.0 h1:BJkq1tgAzqTaDd52MtijWF5UxL0yx644P/+p9HkXmX8= +github.com/mokiat/lacking-gl v0.10.0/go.mod h1:fXyFktRjsM+dBequavgjS2YLQsG7CX5reOQaCd7oO2M= +github.com/mokiat/lacking-js v0.10.0 h1:ePvWensQ7ZIz7KC1ziGJd4xX2DSYRuQJtip/fnjo1Xk= +github.com/mokiat/lacking-js v0.10.0/go.mod h1:EiXawgAtxubPsYwcMkXbma+odJ9vR7ClLBXn4ljbmhs= +github.com/mokiat/wasmgl v0.5.0 h1:LilOv5A2jp2T51pWRb5xZdgl01z6+J9l/AhmdIxHNa0= +github.com/mokiat/wasmgl v0.5.0/go.mod h1:Ikx+bDVpuq+OkvdDDwKI0pIluqK+kduVHJhRFBmmGrw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= +github.com/onsi/gomega v1.27.5 h1:T/X6I0RNFw/kTqgfkZPcQ5KU6vCnWNBGdtrIx2dpGeQ= +github.com/qmuntal/gltf v0.23.1 h1:R8vkbJXmARbD/oI+Yn3252I2qDQ8mljsc88BJJEdYMY= +github.com/qmuntal/gltf v0.23.1/go.mod h1:7FR0CRHoOehIgKTBVq/QVyvPn0i6tzp2AdIghb2bPg4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0= -golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= -golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/netlib v0.0.0-20200229103305-d71f404090bf/go.mod h1:6EVtvAMWMjOBOsTVX0xrjO4A6ULtEgWtAWHzqxDWdJs= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/ecscomp/camera.go b/internal/ecscomp/camera.go deleted file mode 100644 index 63f4f6c..0000000 --- a/internal/ecscomp/camera.go +++ /dev/null @@ -1,27 +0,0 @@ -package ecscomp - -import ( - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/graphics" -) - -func SetCameraStand(entity *ecs.Entity, component *CameraStand) { - entity.SetComponent(CameraStandComponentID, component) -} - -func GetCameraStand(entity *ecs.Entity) *CameraStand { - component := entity.Component(CameraStandComponentID) - if component == nil { - return nil - } - return component.(*CameraStand) -} - -type CameraStand struct { - Target *ecs.Entity - AnchorPosition sprec.Vec3 - AnchorDistance float32 - CameraDistance float32 - Camera *graphics.Camera -} diff --git a/internal/ecscomp/identifiers.go b/internal/ecscomp/identifiers.go deleted file mode 100644 index 91a516c..0000000 --- a/internal/ecscomp/identifiers.go +++ /dev/null @@ -1,13 +0,0 @@ -package ecscomp - -import ( - "github.com/mokiat/lacking/game/ecs" -) - -const ( - PhysicsComponentID ecs.ComponentTypeID = iota - RenderComponentID - VehicleComponentID - PlayerControlComponentID - CameraStandComponentID -) diff --git a/internal/ecscomp/physics.go b/internal/ecscomp/physics.go deleted file mode 100644 index ed5a78e..0000000 --- a/internal/ecscomp/physics.go +++ /dev/null @@ -1,22 +0,0 @@ -package ecscomp - -import ( - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/physics" -) - -type Physics struct { - Body *physics.Body -} - -func SetPhysics(entity *ecs.Entity, component *Physics) { - entity.SetComponent(PhysicsComponentID, component) -} - -func GetPhysics(entity *ecs.Entity) *Physics { - component := entity.Component(PhysicsComponentID) - if component == nil { - return nil - } - return component.(*Physics) -} diff --git a/internal/ecscomp/player.go b/internal/ecscomp/player.go deleted file mode 100644 index 94a40b5..0000000 --- a/internal/ecscomp/player.go +++ /dev/null @@ -1,17 +0,0 @@ -package ecscomp - -import "github.com/mokiat/lacking/game/ecs" - -func SetPlayerControl(entity *ecs.Entity, component *PlayerControl) { - entity.SetComponent(PlayerControlComponentID, component) -} - -func GetPlayerControl(entity *ecs.Entity) *PlayerControl { - component := entity.Component(PlayerControlComponentID) - if component == nil { - return nil - } - return component.(*PlayerControl) -} - -type PlayerControl struct{} diff --git a/internal/ecscomp/render.go b/internal/ecscomp/render.go deleted file mode 100644 index 4a9ec0f..0000000 --- a/internal/ecscomp/render.go +++ /dev/null @@ -1,22 +0,0 @@ -package ecscomp - -import ( - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/graphics" -) - -func SetRender(entity *ecs.Entity, component *Render) { - entity.SetComponent(RenderComponentID, component) -} - -func GetRender(entity *ecs.Entity) *Render { - component := entity.Component(RenderComponentID) - if component == nil { - return nil - } - return component.(*Render) -} - -type Render struct { - Mesh *graphics.Mesh -} diff --git a/internal/ecscomp/vehicle.go b/internal/ecscomp/vehicle.go deleted file mode 100644 index b801252..0000000 --- a/internal/ecscomp/vehicle.go +++ /dev/null @@ -1,42 +0,0 @@ -package ecscomp - -import ( - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/physics" - "github.com/mokiat/lacking/game/physics/solver" -) - -func SetVehicle(entity *ecs.Entity, component *Vehicle) { - entity.SetComponent(VehicleComponentID, component) -} - -func GetVehicle(entity *ecs.Entity) *Vehicle { - component := entity.Component(VehicleComponentID) - if component == nil { - return nil - } - return component.(*Vehicle) -} - -type Vehicle struct { - MaxSteeringAngle sprec.Angle - SteeringAngle sprec.Angle - Acceleration float32 - Deceleration float32 - Recover bool - - Chassis *Chassis - Wheels []*Wheel -} - -type Chassis struct { - Body *physics.Body -} - -type Wheel struct { - Body *physics.Body - RotationConstraint *solver.MatchAxis - AccelerationVelocity float32 - DecelerationVelocity float32 -} diff --git a/internal/ecssys/render.go b/internal/ecssys/render.go deleted file mode 100644 index 240ebc7..0000000 --- a/internal/ecssys/render.go +++ /dev/null @@ -1,36 +0,0 @@ -package ecssys - -import ( - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/rally-mka/internal/ecscomp" -) - -func NewRenderer(ecsScene *ecs.Scene) *Renderer { - return &Renderer{ - ecsScene: ecsScene, - } -} - -type Renderer struct { - ecsScene *ecs.Scene -} - -func (r *Renderer) Update() { - result := r.ecsScene.Find(ecs. - Having(ecscomp.PhysicsComponentID). - And(ecscomp.RenderComponentID)) - defer result.Close() - - for result.HasNext() { - entity := result.Next() - - physicsComp := ecscomp.GetPhysics(entity) - body := physicsComp.Body - - renderComp := ecscomp.GetRender(entity) - mesh := renderComp.Mesh - - mesh.SetPosition(body.Position()) - mesh.SetRotation(body.Orientation()) - } -} diff --git a/internal/ecssys/stand.go b/internal/ecssys/stand.go deleted file mode 100644 index 863641e..0000000 --- a/internal/ecssys/stand.go +++ /dev/null @@ -1,153 +0,0 @@ -package ecssys - -import ( - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/app" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/rally-mka/internal/ecscomp" -) - -func NewCameraStandSystem(ecsScene *ecs.Scene) *CameraStandSystem { - return &CameraStandSystem{ - ecsScene: ecsScene, - zoom: 1.0, - } -} - -type CameraStandSystem struct { - ecsScene *ecs.Scene - zoom float32 - - isRotateLeft bool - isRotateRight bool - isRotateUp bool - isRotateDown bool - isZoomIn bool - isZoomOut bool - - rotationX sprec.Angle - rotationY sprec.Angle -} - -func (s *CameraStandSystem) OnKeyboardEvent(event app.KeyboardEvent) bool { - active := event.Type != app.KeyboardEventTypeKeyUp - switch event.Code { - case app.KeyCodeA: - s.isRotateLeft = active - return true - case app.KeyCodeD: - s.isRotateRight = active - return true - case app.KeyCodeW: - s.isRotateUp = active - return true - case app.KeyCodeS: - s.isRotateDown = active - return true - case app.KeyCodeE: - s.isZoomIn = active - return true - case app.KeyCodeQ: - s.isZoomOut = active - return true - } - return false -} - -func (s *CameraStandSystem) Update(elapsedSeconds float32, gamepad *app.GamepadState) { - result := s.ecsScene.Find(ecs.Having(ecscomp.CameraStandComponentID)) - defer result.Close() - - for result.HasNext() { - entity := result.Next() - cameraStand := ecscomp.GetCameraStand(entity) - s.updateCameraStand(cameraStand, elapsedSeconds, gamepad) - } -} - -func (s *CameraStandSystem) updateCameraStand(cameraStand *ecscomp.CameraStand, elapsedSeconds float32, gamepad *app.GamepadState) { - var ( - targetPhysicsComp = ecscomp.GetPhysics(cameraStand.Target) - targetRenderComp = ecscomp.GetRender(cameraStand.Target) - ) - - var targetPosition sprec.Vec3 - switch { - case targetPhysicsComp != nil: - targetPosition = targetPhysicsComp.Body.Position() - case targetRenderComp != nil: - targetPosition = targetRenderComp.Mesh.Position() - } - // we use a camera anchor to achieve the smooth effect of a - // camera following the target - anchorVector := sprec.Vec3Diff(cameraStand.AnchorPosition, targetPosition) - anchorVector = sprec.ResizedVec3(anchorVector, cameraStand.AnchorDistance) - - cameraVectorZ := anchorVector - cameraVectorX := sprec.Vec3Cross(sprec.BasisYVec3(), cameraVectorZ) - cameraVectorY := sprec.Vec3Cross(cameraVectorZ, cameraVectorX) - - if s.isRotateLeft { - s.rotationY -= sprec.Degrees(elapsedSeconds * 100) - } - if s.isRotateRight { - s.rotationY += sprec.Degrees(elapsedSeconds * 100) - } - if s.isRotateUp { - s.rotationX -= sprec.Degrees(elapsedSeconds * 100) - } - if s.isRotateDown { - s.rotationX += sprec.Degrees(elapsedSeconds * 100) - } - if s.isZoomIn { - s.zoom -= 0.3 * elapsedSeconds * s.zoom - } - if s.isZoomOut { - s.zoom += 0.3 * elapsedSeconds * s.zoom - } - - if gamepad != nil { - if gamepad.RightBumper { - s.zoom = s.zoom - 0.3*elapsedSeconds*s.zoom - } - if gamepad.LeftBumper { - s.zoom = s.zoom + 0.3*elapsedSeconds*s.zoom - } - - rotationAmount := 200 * elapsedSeconds - if sprec.Abs(gamepad.RightStickY) > 0.2 { - rotation := sprec.RotationQuat(sprec.Degrees(gamepad.RightStickY*rotationAmount), cameraVectorX) - anchorVector = sprec.QuatVec3Rotation(rotation, anchorVector) - } - if sprec.Abs(gamepad.RightStickX) > 0.2 { - rotation := sprec.RotationQuat(sprec.Degrees(-gamepad.RightStickX*rotationAmount), cameraVectorY) - anchorVector = sprec.QuatVec3Rotation(rotation, anchorVector) - } - } - - cameraStand.AnchorPosition = sprec.Vec3Sum(targetPosition, anchorVector) - // cameraStand.AnchorPosition = sprec.NewVec3(10.0, 60.0, 40.0) - - // the following approach of creating the view matrix coordinates will fail - // if the camera is pointing directly up or down - cameraVectorZ = anchorVector - cameraVectorX = sprec.Vec3Cross(sprec.BasisYVec3(), cameraVectorZ) - cameraVectorY = sprec.Vec3Cross(cameraVectorZ, cameraVectorX) - - matrix := sprec.Mat4MultiProd( - sprec.TranslationMat4(targetPosition.X, targetPosition.Y, targetPosition.Z), - sprec.TransformationMat4( - sprec.UnitVec3(cameraVectorX), - sprec.UnitVec3(cameraVectorY), - sprec.UnitVec3(cameraVectorZ), - sprec.ZeroVec3(), - ), - sprec.RotationMat4(s.rotationY, 0.0, 1.0, 0.0), - sprec.RotationMat4(sprec.Degrees(-25.0), 1.0, 0.0, 0.0), - sprec.RotationMat4(s.rotationX, 1.0, 0.0, 0.0), - sprec.TranslationMat4(0.0, 0.0, cameraStand.CameraDistance*s.zoom), - ) - - cameraStand.Camera.SetPosition(matrix.Translation()) - cameraStand.Camera.SetRotation(matrix.RotationQuat()) -} diff --git a/internal/ecssys/vehicle.go b/internal/ecssys/vehicle.go deleted file mode 100644 index e071c5c..0000000 --- a/internal/ecssys/vehicle.go +++ /dev/null @@ -1,162 +0,0 @@ -package ecssys - -import ( - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/app" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/rally-mka/internal/ecscomp" -) - -const ( - steeringSpeed = 80 - steeringRestoreSpeed = steeringSpeed * 2 -) - -func NewVehicleSystem(ecsScene *ecs.Scene) *VehicleSystem { - return &VehicleSystem{ - ecsScene: ecsScene, - } -} - -type VehicleSystem struct { - ecsScene *ecs.Scene - - isSteerLeft bool - isSteerRight bool - isAccelerate bool - isDecelerate bool - isRecover bool -} - -func (s *VehicleSystem) OnKeyboardEvent(event app.KeyboardEvent) bool { - active := event.Type != app.KeyboardEventTypeKeyUp - switch event.Code { - case app.KeyCodeArrowLeft: - s.isSteerLeft = active - return true - case app.KeyCodeArrowRight: - s.isSteerRight = active - return true - case app.KeyCodeArrowUp: - s.isAccelerate = active - return true - case app.KeyCodeArrowDown: - s.isDecelerate = active - return true - case app.KeyCodeEnter: - s.isRecover = active - return true - } - return false -} - -func (s *VehicleSystem) Update(elapsedSeconds float32, gamepad *app.GamepadState) { - result := s.ecsScene.Find(ecs.Having(ecscomp.VehicleComponentID)) - defer result.Close() - - for result.HasNext() { - entity := result.Next() - vehicle := ecscomp.GetVehicle(entity) - if ecscomp.GetPlayerControl(entity) != nil { - if gamepad != nil { - s.updateVehicleControlGamepad(vehicle, elapsedSeconds, gamepad) - } else { - s.updateVehicleControlKeyboard(vehicle, elapsedSeconds) - } - } - s.updateVehiclePhysics(vehicle, elapsedSeconds) - } -} - -func (s *VehicleSystem) updateVehicleControlGamepad(vehicle *ecscomp.Vehicle, elapsedSeconds float32, gamepad *app.GamepadState) { - steeringAmount := gamepad.LeftStickX * sprec.Abs(gamepad.LeftStickX) - vehicle.SteeringAngle = -sprec.Degrees(steeringAmount * vehicle.MaxSteeringAngle.Degrees()) - vehicle.Acceleration = gamepad.RightTrigger - vehicle.Deceleration = gamepad.LeftTrigger - vehicle.Recover = gamepad.CrossButton -} - -func (s *VehicleSystem) updateVehicleControlKeyboard(vehicle *ecscomp.Vehicle, elapsedSeconds float32) { - vehicle.Recover = s.isRecover - - autoMaxSteeringAngle := sprec.Degrees(vehicle.MaxSteeringAngle.Degrees() / (1.0 + 0.05*vehicle.Chassis.Body.Velocity().Length())) - switch { - case s.isSteerLeft == s.isSteerRight: - if vehicle.SteeringAngle > 0.001 { - vehicle.SteeringAngle -= sprec.Degrees(elapsedSeconds * steeringRestoreSpeed) - if vehicle.SteeringAngle < 0.0 { - vehicle.SteeringAngle = 0.0 - } - } - if vehicle.SteeringAngle < -0.001 { - vehicle.SteeringAngle += sprec.Degrees(elapsedSeconds * steeringRestoreSpeed) - if vehicle.SteeringAngle > 0.0 { - vehicle.SteeringAngle = 0.0 - } - } - case s.isSteerLeft: - vehicle.SteeringAngle += sprec.Degrees(elapsedSeconds * steeringSpeed) - if vehicle.SteeringAngle > autoMaxSteeringAngle { - vehicle.SteeringAngle = autoMaxSteeringAngle - } - case s.isSteerRight: - vehicle.SteeringAngle -= sprec.Degrees(elapsedSeconds * steeringSpeed) - if vehicle.SteeringAngle < -autoMaxSteeringAngle { - vehicle.SteeringAngle = -autoMaxSteeringAngle - } - } - - if s.isAccelerate { - vehicle.Acceleration = 0.8 - } else { - vehicle.Acceleration = 0.0 - } - if s.isDecelerate { - vehicle.Deceleration = 0.8 - } else { - vehicle.Deceleration = 0.0 - } -} - -func (s *VehicleSystem) updateVehiclePhysics(vehicle *ecscomp.Vehicle, elapsedSeconds float32) { - if vehicle.Recover { - vehicle.Chassis.Body.SetAngularVelocity(sprec.Vec3Sum(vehicle.Chassis.Body.AngularVelocity(), sprec.NewVec3(0.0, 0.0, 0.1))) - vehicle.Chassis.Body.SetVelocity(sprec.Vec3Sum(vehicle.Chassis.Body.Velocity(), sprec.NewVec3(0.0, 0.2, 0.0))) - } - - steeringQuat := sprec.RotationQuat(vehicle.SteeringAngle, sprec.BasisYVec3()) - isMovingForward := sprec.Vec3Dot(vehicle.Chassis.Body.Velocity(), vehicle.Chassis.Body.Orientation().OrientationZ()) > 5.0 - isMovingBackward := sprec.Vec3Dot(vehicle.Chassis.Body.Velocity(), vehicle.Chassis.Body.Orientation().OrientationZ()) < -5.0 - - for _, wheel := range vehicle.Wheels { - if wheel.RotationConstraint != nil { - wheel.RotationConstraint.SetPrimaryAxis(sprec.QuatVec3Rotation(steeringQuat, sprec.BasisXVec3())) - } - - if vehicle.Acceleration > 0.0 { - if isMovingBackward { - if wheelVelocity := sprec.Vec3Dot(wheel.Body.AngularVelocity(), wheel.Body.Orientation().OrientationX()); wheelVelocity < 0.0 { - correction := sprec.Max(-vehicle.Acceleration*wheel.DecelerationVelocity*elapsedSeconds, wheelVelocity) - wheel.Body.SetAngularVelocity(sprec.Vec3Prod(wheel.Body.AngularVelocity(), 1.0-correction/wheelVelocity)) - } - } else { - wheel.Body.SetAngularVelocity(sprec.Vec3Sum(wheel.Body.AngularVelocity(), - sprec.Vec3Prod(wheel.Body.Orientation().OrientationX(), vehicle.Acceleration*wheel.AccelerationVelocity*elapsedSeconds), - )) - } - } - - if vehicle.Deceleration > 0.0 { - if isMovingForward { - if wheelVelocity := sprec.Vec3Dot(wheel.Body.AngularVelocity(), wheel.Body.Orientation().OrientationX()); wheelVelocity > 0.0 { - correction := sprec.Min(vehicle.Deceleration*wheel.DecelerationVelocity*elapsedSeconds, wheelVelocity) - wheel.Body.SetAngularVelocity(sprec.Vec3Prod(wheel.Body.AngularVelocity(), 1.0-correction/wheelVelocity)) - } - } else { - wheel.Body.SetAngularVelocity(sprec.Vec3Sum(wheel.Body.AngularVelocity(), - sprec.Vec3Prod(wheel.Body.Orientation().OrientationX(), -vehicle.Deceleration*wheel.AccelerationVelocity*elapsedSeconds), - )) - } - } - } -} diff --git a/internal/game/controller.go b/internal/game/controller.go deleted file mode 100644 index 789a2d0..0000000 --- a/internal/game/controller.go +++ /dev/null @@ -1,175 +0,0 @@ -package game - -import ( - "time" - - "github.com/mokiat/lacking/app" - "github.com/mokiat/lacking/game/asset" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/graphics" - "github.com/mokiat/lacking/game/physics" - "github.com/mokiat/lacking/resource" - "github.com/mokiat/rally-mka/internal/ecssys" -) - -func NewController(reg asset.Registry, gfxEngine *graphics.Engine) *Controller { - controller := &Controller{ - gfxEngine: gfxEngine, - physicsEngine: physics.NewEngine(), - ecsEngine: ecs.NewEngine(), - - lastFrameTime: time.Now(), - } - controller.registry = resource.NewRegistry(reg, gfxEngine, controller) - return controller -} - -type Controller struct { - app.NopController - - window app.Window - gfxEngine *graphics.Engine - physicsEngine *physics.Engine - ecsEngine *ecs.Engine - registry *resource.Registry - - lastFrameTime time.Time - freezeFrame bool - - width int - height int - - gfxScene *graphics.Scene - physicsScene *physics.Scene - ecsScene *ecs.Scene - - renderSystem *ecssys.Renderer - vehicleSystem *ecssys.VehicleSystem - cameraStandSystem *ecssys.CameraStandSystem - - camera *graphics.Camera - - OnUpdate func() -} - -func (c *Controller) Schedule(fn func() error) { - c.window.Schedule(fn) -} - -func (c *Controller) Registry() *resource.Registry { - return c.registry -} - -func (c *Controller) GFXEngine() *graphics.Engine { - return c.gfxEngine -} - -func (c *Controller) GFXScene() *graphics.Scene { - return c.gfxScene -} - -func (c *Controller) PhysicsScene() *physics.Scene { - return c.physicsScene -} - -func (c *Controller) ECSScene() *ecs.Scene { - return c.ecsScene -} - -func (c *Controller) RenderSystem() *ecssys.Renderer { - return c.renderSystem -} - -func (c *Controller) VehicleSystem() *ecssys.VehicleSystem { - return c.vehicleSystem -} - -func (c *Controller) CameraStandSystem() *ecssys.CameraStandSystem { - return c.cameraStandSystem -} - -func (c *Controller) Camera() *graphics.Camera { - return c.camera -} - -func (c *Controller) OnCreate(window app.Window) { - c.window = window - c.width, c.height = window.Size() - - c.gfxEngine.Create() - - c.gfxScene = c.gfxEngine.CreateScene() - c.physicsScene = c.physicsEngine.CreateScene(0.015) - c.ecsScene = c.ecsEngine.CreateScene() - - c.camera = c.gfxScene.CreateCamera() - - c.renderSystem = ecssys.NewRenderer(c.ecsScene) - c.vehicleSystem = ecssys.NewVehicleSystem(c.ecsScene) - c.cameraStandSystem = ecssys.NewCameraStandSystem(c.ecsScene) -} - -func (c *Controller) OnResize(window app.Window, width, height int) { - c.width, c.height = width, height -} - -func (c *Controller) OnCloseRequested(window app.Window) { - window.Close() -} - -func (c *Controller) OnKeyboardEvent(window app.Window, event app.KeyboardEvent) bool { - if event.Code == app.KeyCodeEscape { - window.Close() - return true - } - if event.Code == app.KeyCodeF { - switch event.Type { - case app.KeyboardEventTypeKeyDown: - c.freezeFrame = true - return true - case app.KeyboardEventTypeKeyUp: - c.freezeFrame = false - return true - } - } - return c.vehicleSystem.OnKeyboardEvent(event) || - c.cameraStandSystem.OnKeyboardEvent(event) -} - -func (c *Controller) OnRender(window app.Window) { - currentTime := time.Now() - elapsedSeconds := float32(currentTime.Sub(c.lastFrameTime).Seconds()) - c.lastFrameTime = currentTime - - if !c.freezeFrame { - var gamepad *app.GamepadState - if state, ok := window.GamepadState(0); ok { - gamepad = &state - } - - c.physicsScene.Update(elapsedSeconds) - c.vehicleSystem.Update(elapsedSeconds, gamepad) - c.renderSystem.Update() - c.cameraStandSystem.Update(elapsedSeconds, gamepad) - - if c.OnUpdate != nil { - c.OnUpdate() - } - - c.gfxScene.Render(graphics.NewViewport(0, 0, c.width, c.height), c.camera) - } - - window.Invalidate() // force redraw -} - -func (c *Controller) OnDestroy(window app.Window) { - c.renderSystem = nil - c.vehicleSystem = nil - c.cameraStandSystem = nil - - c.ecsScene.Delete() - c.physicsScene.Delete() - c.gfxScene.Delete() - - c.gfxEngine.Destroy() -} diff --git a/internal/game/data/home.go b/internal/game/data/home.go new file mode 100644 index 0000000..d4bccc1 --- /dev/null +++ b/internal/game/data/home.go @@ -0,0 +1,37 @@ +package data + +import ( + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/util/async" +) + +func LoadHomeData(engine *game.Engine, resourceSet *game.ResourceSet) game.Promise[*HomeData] { + scenePromise := resourceSet.OpenSceneByName("Home Screen") + + result := async.NewPromise[*HomeData]() + go func() { + var data HomeData + err := firstErr( + scenePromise.Inject(&data.Scene), + ) + if err != nil { + result.Fail(err) + } else { + result.Deliver(&data) + } + }() + return game.SafePromise(result, engine) +} + +type HomeData struct { + Scene *game.SceneDefinition +} + +func firstErr(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} diff --git a/internal/game/data/play.go b/internal/game/data/play.go new file mode 100644 index 0000000..3799bbc --- /dev/null +++ b/internal/game/data/play.go @@ -0,0 +1,62 @@ +package data + +import ( + "fmt" + + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/util/async" +) + +type Controller string + +const ( + ControllerKeyboard Controller = "keyboard" + ControllerMouse Controller = "mouse" + ControllerGamepad Controller = "gamepad" +) + +type Environment string + +const ( + EnvironmentDay Environment = "day" + EnvironmentNight Environment = "night" +) + +func LoadPlayData(engine *game.Engine, resourceSet *game.ResourceSet, environment Environment, controller Controller) game.Promise[*PlayData] { + var sceneName string + switch environment { + case EnvironmentDay: + sceneName = "Forest-Day" + case EnvironmentNight: + sceneName = "Forest-Night" + default: + panic(fmt.Errorf("unknown environment %q", environment)) + } + + scenePromise := resourceSet.OpenSceneByName(sceneName) + vehiclePromise := resourceSet.OpenModelByName("SUV") + + result := async.NewPromise[*PlayData]() + go func() { + var data PlayData + data.Environment = environment + data.Controller = controller + err := firstErr( + scenePromise.Inject(&data.Scene), + vehiclePromise.Inject(&data.Vehicle), + ) + if err != nil { + result.Fail(err) + } else { + result.Deliver(&data) + } + }() + return game.SafePromise(result, engine) +} + +type PlayData struct { + Scene *game.SceneDefinition + Vehicle *game.ModelDefinition + Environment Environment + Controller Controller +} diff --git a/internal/global/context.go b/internal/global/context.go deleted file mode 100644 index cb6c3d3..0000000 --- a/internal/global/context.go +++ /dev/null @@ -1,10 +0,0 @@ -package global - -import ( - "github.com/mokiat/rally-mka/internal/game" -) - -// TODO: Get rid of this. -type Context struct { - GameController *game.Controller -} diff --git a/internal/scene/car/chassis.go b/internal/scene/car/chassis.go deleted file mode 100644 index 154fb99..0000000 --- a/internal/scene/car/chassis.go +++ /dev/null @@ -1,86 +0,0 @@ -package car - -import ( - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/graphics" - "github.com/mokiat/lacking/game/physics" - "github.com/mokiat/lacking/resource" - "github.com/mokiat/lacking/shape" - "github.com/mokiat/rally-mka/internal/ecscomp" -) - -const ( - chassisRadius = 2 - chassisMass = 1300.0 / 5.0 - chassisMomentOfInertia = chassisMass * chassisRadius * chassisRadius / 5.0 - chassisDragFactor = 0.0 // 0.5 * 6.8 * 1.0 - chassisAngularDragFactor = 0.0 // 0.5 * 6.8 * 1.0 - chassisRestitutionCoef = 0.0 -) - -func Chassis(model *resource.Model) *ChassisBuilder { - return &ChassisBuilder{ - model: model, - } -} - -type ChassisBuilder struct { - model *resource.Model - modifiers []func(entity *ecs.Entity) -} - -func (b *ChassisBuilder) WithName(name string) *ChassisBuilder { - b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { - physicsComponent := ecscomp.GetPhysics(entity) - physicsComponent.Body.SetName(name) - }) - return b -} - -func (b *ChassisBuilder) WithPosition(position sprec.Vec3) *ChassisBuilder { - b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { - physicsComponent := ecscomp.GetPhysics(entity) - physicsComponent.Body.SetPosition(position) - }) - return b -} - -func (b *ChassisBuilder) Build(ecsScene *ecs.Scene, gfxScene *graphics.Scene, physicsScene *physics.Scene) *ecs.Entity { - bodyNode, _ := b.model.FindNode("Chassis") - - physicsBody := physicsScene.CreateBody() - physicsBody.SetPosition(sprec.ZeroVec3()) - physicsBody.SetOrientation(sprec.IdentityQuat()) - physicsBody.SetMass(chassisMass) - physicsBody.SetMomentOfInertia(physics.SymmetricMomentOfInertia(chassisMomentOfInertia)) - physicsBody.SetDragFactor(chassisDragFactor) - physicsBody.SetAngularDragFactor(chassisAngularDragFactor) - physicsBody.SetRestitutionCoefficient(chassisRestitutionCoef) - physicsBody.SetCollisionShapes([]physics.CollisionShape{ - shape.Placement{ - Position: sprec.NewVec3(0.0, 0.3, -0.4), - Orientation: sprec.IdentityQuat(), - Shape: shape.NewStaticBox(1.6, 1.4, 4.0), - }, - }) - - entity := ecsScene.CreateEntity() - ecscomp.SetPhysics(entity, &ecscomp.Physics{ - Body: physicsBody, - }) - - gfxMesh := gfxScene.CreateMesh(bodyNode.Mesh.GFXMeshTemplate) - gfxMesh.SetPosition(bodyNode.Matrix.Translation()) - // TODO: Set Rotation - // TODO: Set Scale - - ecscomp.SetRender(entity, &ecscomp.Render{ - Mesh: gfxMesh, - }) - - for _, modifier := range b.modifiers { - modifier(entity) - } - return entity -} diff --git a/internal/scene/car/wheel.go b/internal/scene/car/wheel.go deleted file mode 100644 index 323399e..0000000 --- a/internal/scene/car/wheel.go +++ /dev/null @@ -1,99 +0,0 @@ -package car - -import ( - "fmt" - - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/graphics" - "github.com/mokiat/lacking/game/physics" - "github.com/mokiat/lacking/resource" - "github.com/mokiat/lacking/shape" - "github.com/mokiat/rally-mka/internal/ecscomp" -) - -const ( - wheelRadius = 0.3 - wheelMass = 20.0 // wheel: ~12kg; rim: ~8kg - wheelMomentOfInertia = wheelMass * wheelRadius * wheelRadius / 2.0 // using cylinder as approximation - wheelDragFactor = 0.0 // 0.5 * 0.3 * 0.8 - wheelAngularDragFactor = 0.0 // 0.5 * 0.3 * 0.8 - wheelRestitutionCoef = 0.0 -) - -type WheelLocation string - -const ( - FrontLeftWheelLocation WheelLocation = "FL" - FrontRightWheelLocation WheelLocation = "FR" - BackLeftWheelLocation WheelLocation = "BL" - BackRightWheelLocation WheelLocation = "BR" -) - -func Wheel(model *resource.Model, location WheelLocation) *WheelBuilder { - return &WheelBuilder{ - model: model, - location: location, - } -} - -type WheelBuilder struct { - model *resource.Model - location WheelLocation - modifiers []func(entity *ecs.Entity) -} - -func (b *WheelBuilder) WithName(name string) *WheelBuilder { - b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { - physicsComponent := ecscomp.GetPhysics(entity) - physicsComponent.Body.SetName(name) - }) - return b -} - -func (b *WheelBuilder) WithPosition(position sprec.Vec3) *WheelBuilder { - b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { - physicsComponent := ecscomp.GetPhysics(entity) - physicsComponent.Body.SetPosition(position) - }) - return b -} - -func (b *WheelBuilder) Build(ecsScene *ecs.Scene, gfxScene *graphics.Scene, physicsScene *physics.Scene) *ecs.Entity { - modelNode, _ := b.model.FindNode(fmt.Sprintf("%sWheel", b.location)) - - physicsBody := physicsScene.CreateBody() - physicsBody.SetPosition(sprec.ZeroVec3()) - physicsBody.SetOrientation(sprec.IdentityQuat()) - physicsBody.SetMass(wheelMass) - physicsBody.SetMomentOfInertia(physics.SymmetricMomentOfInertia(wheelMomentOfInertia)) - physicsBody.SetDragFactor(wheelDragFactor) - physicsBody.SetAngularDragFactor(wheelAngularDragFactor) - physicsBody.SetRestitutionCoefficient(wheelRestitutionCoef) - physicsBody.SetCollisionShapes([]physics.CollisionShape{ - // using sphere shape at is easier to do in physics engine at the moment - shape.Placement{ - Position: sprec.ZeroVec3(), - Orientation: sprec.IdentityQuat(), - Shape: shape.NewStaticSphere(0.3), - }, - }) - - entity := ecsScene.CreateEntity() - ecscomp.SetPhysics(entity, &ecscomp.Physics{ - Body: physicsBody, - }) - - gfxMesh := gfxScene.CreateMesh(modelNode.Mesh.GFXMeshTemplate) - gfxMesh.SetPosition(modelNode.Matrix.Translation()) - // TODO: Set Rotation - // TODO: Set Scale - - ecscomp.SetRender(entity, &ecscomp.Render{ - Mesh: gfxMesh, - }) - for _, modifier := range b.modifiers { - modifier(entity) - } - return entity -} diff --git a/internal/scene/data.go b/internal/scene/data.go deleted file mode 100644 index 7c01741..0000000 --- a/internal/scene/data.go +++ /dev/null @@ -1,60 +0,0 @@ -package scene - -import ( - "math/rand" - "time" - - "github.com/mokiat/lacking/async" - "github.com/mokiat/lacking/resource" -) - -const ( - modelIDSUV = "eaeb7483-7271-441f-a470-c0a8fa225161" - levelIDForest = "884e6395-2300-47bb-9916-b80e3dc0e086" - levelIDHighway = "acf21108-47ad-44ef-ba21-bf5473bfbaa0" - levelIDPlayground = "9ca25b5c-ffa0-4224-ad80-a3c4d67930b7" -) - -func init() { - rand.Seed(time.Now().Unix()) -} - -func NewData(registry *resource.Registry) *Data { - return &Data{ - registry: registry, - } -} - -type Data struct { - registry *resource.Registry - loadOutcome async.Outcome - - CarModel *resource.Model - Level *resource.Level -} - -func (d *Data) Request() async.Outcome { - var levelID string - switch rand.Intn(2) { - case 0: - levelID = levelIDForest - case 1: - levelID = levelIDHighway - default: - levelID = levelIDPlayground - } - d.loadOutcome = async.NewCompositeOutcome( - d.registry.LoadModel(modelIDSUV).OnSuccess(resource.InjectModel(&d.CarModel)), - d.registry.LoadLevel(levelID).OnSuccess(resource.InjectLevel(&d.Level)), // Forest - ) - return d.loadOutcome -} - -func (d *Data) Dismiss() { - d.registry.UnloadModel(d.CarModel) - d.registry.UnloadLevel(d.Level) -} - -func (d *Data) IsAvailable() bool { - return d.loadOutcome.IsAvailable() -} diff --git a/internal/ui/action/view.go b/internal/ui/action/application.go similarity index 100% rename from internal/ui/action/view.go rename to internal/ui/action/application.go diff --git a/internal/ui/action/data.go b/internal/ui/action/data.go deleted file mode 100644 index 040fbea..0000000 --- a/internal/ui/action/data.go +++ /dev/null @@ -1,7 +0,0 @@ -package action - -import "github.com/mokiat/rally-mka/internal/scene" - -type SetGameData struct { - GameData *scene.Data -} diff --git a/internal/ui/bootstrap.go b/internal/ui/bootstrap.go index 092b3a7..756a87a 100644 --- a/internal/ui/bootstrap.go +++ b/internal/ui/bootstrap.go @@ -1,19 +1,22 @@ package internal import ( + "github.com/mokiat/lacking/game" "github.com/mokiat/lacking/ui" co "github.com/mokiat/lacking/ui/component" "github.com/mokiat/lacking/ui/mvc" - "github.com/mokiat/rally-mka/internal/game" - "github.com/mokiat/rally-mka/internal/global" "github.com/mokiat/rally-mka/internal/ui/controller" + "github.com/mokiat/rally-mka/internal/ui/global" "github.com/mokiat/rally-mka/internal/ui/model" "github.com/mokiat/rally-mka/internal/ui/view" ) func BootstrapApplication(window *ui.Window, gameController *game.Controller) { + engine := gameController.Engine() + resourceSet := engine.CreateResourceSet() co.RegisterContext(global.Context{ - GameController: gameController, + Engine: engine, + ResourceSet: resourceSet, }) co.Initialize(window, co.New(Bootstrap, nil)) } diff --git a/internal/ui/controller/application.go b/internal/ui/controller/application.go index dffa76c..5c68379 100644 --- a/internal/ui/controller/application.go +++ b/internal/ui/controller/application.go @@ -21,9 +21,6 @@ func (a *Application) Reduce(act mvc.Action) bool { case action.ChangeView: a.appModel.SetActiveView(act.ViewName) return true - case action.SetGameData: - a.appModel.SetGameData(act.GameData) - return true default: return false } diff --git a/internal/ui/controller/play.go b/internal/ui/controller/play.go new file mode 100644 index 0000000..ddbaf54 --- /dev/null +++ b/internal/ui/controller/play.go @@ -0,0 +1,388 @@ +package controller + +import ( + "runtime" + + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/app" + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/game/ecs" + "github.com/mokiat/lacking/game/graphics" + "github.com/mokiat/lacking/game/physics" + "github.com/mokiat/lacking/game/physics/collision" + "github.com/mokiat/lacking/game/preset" + "github.com/mokiat/lacking/ui" + "github.com/mokiat/rally-mka/internal/game/data" +) + +const ( + anchorDistance = 6.0 + cameraDistance = 10.0 + pitchAngle = 20.0 +) + +func NewPlayController(window app.Window, engine *game.Engine, playData *data.PlayData) *PlayController { + return &PlayController{ + window: window, + engine: engine, + playData: playData, + } +} + +type PlayController struct { + window app.Window + engine *game.Engine + playData *data.PlayData + + preUpdateSubscription *game.UpdateSubscription + postUpdateSubscription *game.UpdateSubscription + + scene *game.Scene + gfxScene *graphics.Scene + physicsScene *physics.Scene + ecsScene *ecs.Scene + + followCameraSystem *preset.FollowCameraSystem + followCamera *graphics.Camera + bonnetCamera *graphics.Camera + + carSystem *preset.CarSystem + vehicleDefinition *preset.CarDefinition + vehicle *preset.Car +} + +func (c *PlayController) Start(environment data.Environment, controller data.Controller) { + physics.ImpulseDriftAdjustmentRatio = 0.06 // FIXME: Use default once multi-point collisions are fixed + + // TODO: These subscriptions should be attached on the scene and/or the physics. + c.preUpdateSubscription = c.engine.SubscribePreUpdate(c.onPreUpdate) + c.postUpdateSubscription = c.engine.SubscribePostUpdate(c.onPostUpdate) + + c.scene = c.engine.CreateScene() + c.scene.Initialize(c.playData.Scene) + + c.gfxScene = c.scene.Graphics() + c.physicsScene = c.scene.Physics() + c.ecsScene = c.scene.ECS() + + c.vehicleDefinition = c.createVehicleDefinition() + + c.followCameraSystem = preset.NewFollowCameraSystem(c.ecsScene, c.window) + c.followCameraSystem.UseDefaults() + + c.carSystem = preset.NewCarSystem(c.ecsScene, c.gfxScene, c.window) + + var sunLight *graphics.DirectionalLight + switch environment { + case data.EnvironmentDay: + sunLight = c.scene.Graphics().CreateDirectionalLight(graphics.DirectionalLightInfo{ + EmitColor: dprec.NewVec3(0.5, 0.5, 0.3), + EmitRange: 16000, // FIXME + }) + + case data.EnvironmentNight: + sunLight = c.scene.Graphics().CreateDirectionalLight(graphics.DirectionalLightInfo{ + EmitColor: dprec.NewVec3(0.001, 0.001, 0.001), + EmitRange: 16000, // FIXME + }) + } + + lightNode := game.NewNode() + lightRotation := dprec.QuatProd( + dprec.RotationQuat(dprec.Degrees(-140), dprec.BasisYVec3()), + dprec.RotationQuat(dprec.Degrees(-45), dprec.BasisXVec3()), + ) + lightPosition := dprec.QuatVec3Rotation(lightRotation, dprec.NewVec3(0.0, 0.0, 100.0)) + lightNode.SetPosition(lightPosition) + lightNode.SetRotation(lightRotation) + lightNode.UseTransformation(func(parent, current dprec.Mat4) dprec.Mat4 { + // Remove parent's rotation + parent.M11 = 1.0 + parent.M12 = 0.0 + parent.M13 = 0.0 + parent.M21 = 0.0 + parent.M22 = 1.0 + parent.M23 = 0.0 + parent.M31 = 0.0 + parent.M32 = 0.0 + parent.M33 = 1.0 + return dprec.Mat4Prod(parent, current) + }) + lightNode.SetAttachable(sunLight) + + carInstance := c.scene.CreateModel(game.ModelInfo{ + Name: "SUV", + Definition: c.playData.Vehicle, + Position: dprec.ZeroVec3(), + Rotation: dprec.IdentityQuat(), + Scale: dprec.NewVec3(1.0, 1.0, 1.0), + IsDynamic: true, + }) + c.vehicle = c.vehicleDefinition.ApplyToModel(c.scene, preset.CarApplyInfo{ + Model: carInstance, + Position: dprec.NewVec3(0.0, 0.5, 0.0), + Rotation: dprec.IdentityQuat(), + }) + var vehicleNodeComponent *preset.NodeComponent + ecs.FetchComponent(c.vehicle.Entity(), &vehicleNodeComponent) + vehicleNode := vehicleNodeComponent.Node + vehicleNode.AppendChild(lightNode) // FIXME + + var vehicleCarComponent *preset.CarComponent + ecs.FetchComponent(c.vehicle.Entity(), &vehicleCarComponent) + vehicleCarComponent.LightsOn = (environment == data.EnvironmentNight) + + switch controller { + case data.ControllerKeyboard: + ecs.AttachComponent(c.vehicle.Entity(), &preset.CarKeyboardControl{ + AccelerateKey: ui.KeyCodeArrowUp, + DecelerateKey: ui.KeyCodeArrowDown, + TurnLeftKey: ui.KeyCodeArrowLeft, + TurnRightKey: ui.KeyCodeArrowRight, + ShiftUpKey: ui.KeyCodeA, + ShiftDownKey: ui.KeyCodeZ, + RecoverKey: ui.KeyCodeLeftShift, + + AccelerationChangeSpeed: 2.0, + DecelerationChangeSpeed: 4.0, + SteeringChangeSpeed: 3.0, + SteeringRestoreSpeed: 6.0, + }) + case data.ControllerMouse: + ecs.AttachComponent(c.vehicle.Entity(), &preset.CarMouseControl{ + AccelerationChangeSpeed: 2.0, + DecelerationChangeSpeed: 4.0, + Destination: dprec.ZeroVec3(), + }) + case data.ControllerGamepad: + ecs.AttachComponent(c.vehicle.Entity(), &preset.CarGamepadControl{ + Gamepad: c.window.Gamepads()[0], + }) + } + + c.followCamera = c.gfxScene.CreateCamera() + c.followCamera.SetFoVMode(graphics.FoVModeHorizontalPlus) + c.followCamera.SetFoV(sprec.Degrees(66)) + c.followCamera.SetAutoExposure(false) + c.followCamera.SetExposure(15.0) + c.followCamera.SetAutoFocus(false) + c.gfxScene.SetActiveCamera(c.followCamera) + + followCameraNode := game.NewNode() + followCameraNode.SetPosition(dprec.NewVec3(0.0, 20.0, 10.0)) + followCameraNode.SetAttachable(c.followCamera) + c.scene.Root().AppendChild(followCameraNode) + + c.bonnetCamera = c.gfxScene.CreateCamera() + c.bonnetCamera.SetFoVMode(graphics.FoVModeHorizontalPlus) + c.bonnetCamera.SetFoV(sprec.Degrees(80)) + c.bonnetCamera.SetAutoExposure(false) + c.bonnetCamera.SetExposure(15.0) + c.bonnetCamera.SetAutoFocus(false) + + bonnetCameraNode := game.NewNode() + bonnetCameraNode.SetAttachable(c.bonnetCamera) + bonnetCameraNode.SetRotation(dprec.RotationQuat(dprec.Degrees(180), dprec.BasisYVec3())) + bonnetCameraNode.SetPosition(dprec.NewVec3(0.0, 0.75, 0.35)) + vehicleNode.AppendChild(bonnetCameraNode) + + followCameraEntity := c.ecsScene.CreateEntity() + ecs.AttachComponent(followCameraEntity, &preset.NodeComponent{ + Node: followCameraNode, + }) + ecs.AttachComponent(followCameraEntity, &preset.ControlledComponent{ + Inputs: preset.ControlInputKeyboard | preset.ControlInputMouse | preset.ControlInputGamepad0, + }) + ecs.AttachComponent(followCameraEntity, &preset.FollowCameraComponent{ + Target: vehicleNode, + AnchorPosition: dprec.Vec3Sum(vehicleNode.Position(), dprec.NewVec3(0.0, 0.0, -anchorDistance)), + AnchorDistance: anchorDistance, + CameraDistance: cameraDistance, + PitchAngle: dprec.Degrees(-pitchAngle), + YawAngle: dprec.Degrees(0), + Zoom: 1.0, + }) + + runtime.GC() + c.engine.ResetDeltaTime() +} + +func (c *PlayController) Stop() { + c.preUpdateSubscription.Delete() + c.postUpdateSubscription.Delete() + c.scene.Delete() +} + +func (c *PlayController) Pause() { + c.scene.Freeze() +} + +func (c *PlayController) Resume() { + c.scene.Unfreeze() +} + +func (c *PlayController) Velocity() float64 { + if c.vehicle == nil { + return 0.0 + } + return c.vehicle.Velocity() +} + +func (c *PlayController) ToggleCamera() { + if c.scene.Graphics().ActiveCamera() == c.followCamera { + c.scene.Graphics().SetActiveCamera(c.bonnetCamera) + } else { + c.scene.Graphics().SetActiveCamera(c.followCamera) + } +} + +func (c *PlayController) IsDrive() bool { + if c.vehicle == nil { + return true + } + var carComp *preset.CarComponent + ecs.FetchComponent(c.vehicle.Entity(), &carComp) + return carComp.Gear == preset.CarGearForward +} + +func (c *PlayController) OnMouseEvent(element *ui.Element, event ui.MouseEvent) bool { + return c.carSystem.OnMouseEvent(element, event) +} + +func (c *PlayController) OnKeyboardEvent(event ui.KeyboardEvent) bool { + return c.carSystem.OnKeyboardEvent(event) +} + +func (c *PlayController) onPreUpdate(engine *game.Engine, scene *game.Scene, elapsedSeconds float64) { + // TODO: This check will not be necessary if subscription is on Scene. + if !c.scene.IsFrozen() { + c.carSystem.Update(elapsedSeconds) + } +} + +func (c *PlayController) onPostUpdate(engine *game.Engine, scene *game.Scene, elapsedSeconds float64) { + // TODO: This check will not be necessary if subscription is on Scene. + if !c.scene.IsFrozen() { + c.followCameraSystem.Update(elapsedSeconds) + } +} + +func (c *PlayController) createVehicleDefinition() *preset.CarDefinition { + collisionGroup := physics.NewCollisionGroup() + + chassisBodyDef := c.physicsScene.Engine().CreateBodyDefinition(physics.BodyDefinitionInfo{ + Mass: 260, + MomentOfInertia: physics.SymmetricMomentOfInertia(208), + DragFactor: 0.0, + AngularDragFactor: 0.0, + RestitutionCoefficient: 0.0, + CollisionGroup: collisionGroup, + CollisionBoxes: []collision.Box{ + collision.NewBox( + dprec.NewVec3(0.0, 0.34, -0.3), + dprec.IdentityQuat(), + dprec.NewVec3(1.6, 1.18, 3.64), + ), + }, + }) + + wheelBodyDef := c.physicsScene.Engine().CreateBodyDefinition(physics.BodyDefinitionInfo{ + Mass: 20, + MomentOfInertia: physics.SymmetricMomentOfInertia(0.9), + DragFactor: 0.0, + AngularDragFactor: 0.0, + RestitutionCoefficient: 0.0, + CollisionGroup: collisionGroup, + CollisionSpheres: []collision.Sphere{ + collision.NewSphere(dprec.ZeroVec3(), 0.25), + }, + }) + + hubBodyDef := c.physicsScene.Engine().CreateBodyDefinition(physics.BodyDefinitionInfo{ + Mass: 1, + MomentOfInertia: physics.SymmetricMomentOfInertia(0.01), + DragFactor: 0.0, + AngularDragFactor: 0.0, + RestitutionCoefficient: 0.0, + }) + + chassisDef := preset.NewChassisDefinition(). + WithNodeName("Chassis"). + WithBodyDefinition(chassisBodyDef). + WithHeadLightNodeNames("FLLight", "FRLight"). + WithTailLightNodeNames("BLLight", "BRLight"). + WithBeamLightNodeNames("FLBeamLight", "FRBeamLight"). + WithStopLightNodeNames("BLStopLight", "BRStopLight") + + frontLeftWheelDef := preset.NewWheelDefinition(). + WithNodeName("FLWheel"). + WithBodyDefinition(wheelBodyDef) + + frontRightWheelDef := preset.NewWheelDefinition(). + WithNodeName("FRWheel"). + WithBodyDefinition(wheelBodyDef) + + rearLeftWheelDef := preset.NewWheelDefinition(). + WithNodeName("BLWheel"). + WithBodyDefinition(wheelBodyDef) + + rearRightWheelDef := preset.NewWheelDefinition(). + WithNodeName("BRWheel"). + WithBodyDefinition(wheelBodyDef) + + frontLeftHubDef := preset.NewHubDefinition(). + WithNodeName("FLHub"). + WithBodyDefinition(hubBodyDef) + + frontRightHubDef := preset.NewHubDefinition(). + WithNodeName("FRHub"). + WithBodyDefinition(hubBodyDef) + + rearLeftHubDef := preset.NewHubDefinition(). + WithNodeName("BLHub"). + WithBodyDefinition(hubBodyDef) + + rearRightHubDef := preset.NewHubDefinition(). + WithNodeName("BRHub"). + WithBodyDefinition(hubBodyDef) + + frontAxisDef := preset.NewAxisDefinition(). + WithPosition(dprec.NewVec3(0.0, -0.18, 0.96)). + WithWidth(1.7). + WithSuspensionLength(0.16). + WithSpringLength(0.25). + WithSpringFrequency(2.9). + WithSpringDamping(0.8). + WithLeftWheelDefinition(frontLeftWheelDef). + WithRightWheelDefinition(frontRightWheelDef). + WithLeftHubDefinition(frontLeftHubDef). + WithRightHubDefinition(frontRightHubDef). + WithMaxSteeringAngle(dprec.Degrees(45)). + WithMaxAcceleration(145). + WithMaxBraking(250). + WithReverseRatio(0.5) + + rearAxisDef := preset.NewAxisDefinition(). + WithPosition(dprec.NewVec3(0.0, -0.18, -1.37)). + WithWidth(1.7). + WithSuspensionLength(0.16). + WithSpringLength(0.25). + WithSpringFrequency(2.7). + WithSpringDamping(0.8). + WithLeftWheelDefinition(rearLeftWheelDef). + WithRightWheelDefinition(rearRightWheelDef). + WithLeftHubDefinition(rearLeftHubDef). + WithRightHubDefinition(rearRightHubDef). + WithMaxSteeringAngle(dprec.Degrees(0)). + WithMaxAcceleration(145). + WithMaxBraking(180). + WithReverseRatio(0.5) + + carDef := preset.NewCarDefinition(). + WithChassisDefinition(chassisDef). + WithAxisDefinition(frontAxisDef). + WithAxisDefinition(rearAxisDef) + + return carDef +} diff --git a/internal/ui/global/context.go b/internal/ui/global/context.go new file mode 100644 index 0000000..acb2136 --- /dev/null +++ b/internal/ui/global/context.go @@ -0,0 +1,8 @@ +package global + +import "github.com/mokiat/lacking/game" + +type Context struct { + Engine *game.Engine + ResourceSet *game.ResourceSet +} diff --git a/internal/ui/home/view.go b/internal/ui/home/view.go deleted file mode 100644 index 1f9276a..0000000 --- a/internal/ui/home/view.go +++ /dev/null @@ -1,128 +0,0 @@ -package home - -import ( - "github.com/mokiat/lacking/log" - "github.com/mokiat/lacking/ui" - co "github.com/mokiat/lacking/ui/component" - "github.com/mokiat/lacking/ui/mat" - "github.com/mokiat/lacking/ui/mvc" - "github.com/mokiat/lacking/util/optional" - "github.com/mokiat/rally-mka/internal/ui/action" - "github.com/mokiat/rally-mka/internal/ui/model" - "github.com/mokiat/rally-mka/internal/ui/widget" -) - -var View = co.Define(func(props co.Properties, scope co.Scope) co.Instance { - onContinueClicked := func() { - log.Info("Continue") - } - - onNewGameClicked := func() { - log.Info("New Game") - mvc.Dispatch(scope, action.ChangeView{ - ViewName: model.ViewNamePlay, - }) - } - - onLoadGameClicked := func() { - log.Info("Load Game") - } - - onOptionsClicked := func() { - log.Info("Options") - } - - return co.New(mat.Container, func() { - co.WithData(mat.ContainerData{ - BackgroundColor: optional.Value(ui.Black()), - Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), - }) - - co.WithChild("background-picture", co.New(mat.Picture, func() { - co.WithData(mat.PictureData{ - Image: co.OpenImage(scope, "ui/images/background.png"), - Mode: mat.ImageModeCover, - }) - co.WithLayoutData(mat.LayoutData{ - Top: optional.Value(0), - Bottom: optional.Value(0), - Left: optional.Value(250), - Right: optional.Value(0), - }) - })) - - co.WithChild("button-holder", co.New(mat.Container, func() { - co.WithData(mat.ContainerData{ - Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), - }) - co.WithLayoutData(mat.LayoutData{ - Left: optional.Value(100), - VerticalCenter: optional.Value(0), - Width: optional.Value(300), - Height: optional.Value(200), - }) - - buttonWidth := optional.Value(130) - - co.WithChild("continue-button", co.New(widget.HomeButton, func() { - co.WithData(widget.HomeButtonData{ - Text: "Continue", - }) - co.WithLayoutData(mat.LayoutData{ - Top: optional.Value(0), - Left: optional.Value(0), - Width: buttonWidth, - Height: optional.Value(30), - }) - co.WithCallbackData(widget.HomeButtonCallbackData{ - ClickListener: onContinueClicked, - }) - })) - - co.WithChild("new-game-button", co.New(widget.HomeButton, func() { - co.WithData(widget.HomeButtonData{ - Text: "New Game", - }) - co.WithLayoutData(mat.LayoutData{ - Top: optional.Value(50), - Left: optional.Value(0), - Width: buttonWidth, - Height: optional.Value(30), - }) - co.WithCallbackData(widget.HomeButtonCallbackData{ - ClickListener: onNewGameClicked, - }) - })) - - co.WithChild("load-game-button", co.New(widget.HomeButton, func() { - co.WithData(widget.HomeButtonData{ - Text: "Load Game", - }) - co.WithLayoutData(mat.LayoutData{ - Top: optional.Value(100), - Left: optional.Value(0), - Width: buttonWidth, - Height: optional.Value(30), - }) - co.WithCallbackData(widget.HomeButtonCallbackData{ - ClickListener: onLoadGameClicked, - }) - })) - - co.WithChild("options-button", co.New(widget.HomeButton, func() { - co.WithData(widget.HomeButtonData{ - Text: "Options", - }) - co.WithLayoutData(mat.LayoutData{ - Top: optional.Value(150), - Left: optional.Value(0), - Width: buttonWidth, - Height: optional.Value(30), - }) - co.WithCallbackData(widget.HomeButtonCallbackData{ - ClickListener: onOptionsClicked, - }) - })) - })) - }) -}) diff --git a/internal/ui/intro/view.go b/internal/ui/intro/view.go deleted file mode 100644 index 50a56da..0000000 --- a/internal/ui/intro/view.go +++ /dev/null @@ -1,66 +0,0 @@ -package intro - -import ( - "fmt" - - "github.com/mokiat/lacking/ui" - co "github.com/mokiat/lacking/ui/component" - "github.com/mokiat/lacking/ui/mat" - "github.com/mokiat/lacking/ui/mvc" - "github.com/mokiat/lacking/util/optional" - "github.com/mokiat/rally-mka/internal/global" - "github.com/mokiat/rally-mka/internal/scene" - "github.com/mokiat/rally-mka/internal/ui/action" - "github.com/mokiat/rally-mka/internal/ui/model" -) - -var View = co.Define(func(props co.Properties, scope co.Scope) co.Instance { - context := co.GetContext[global.Context]() - - co.Once(func() { - co.Window(scope).SetCursorVisible(false) - }) - - co.Defer(func() { - co.Window(scope).SetCursorVisible(true) - }) - - co.Once(func() { - gameData := scene.NewData( - context.GameController.Registry(), - ) - gameData.Request().OnSuccess(func(interface{}) { - co.Schedule(func() { - mvc.Dispatch(scope, action.SetGameData{ - GameData: gameData, - }) - mvc.Dispatch(scope, action.ChangeView{ - ViewName: model.ViewNameHome, - }) - }) - }).OnError(func(err error) { - panic(fmt.Errorf("failed to load assets: %w", err)) - }) - }) - - return co.New(mat.Container, func() { - co.WithData(mat.ContainerData{ - BackgroundColor: optional.Value(ui.Black()), - Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), - }) - - co.WithChild("logo-picture", co.New(mat.Picture, func() { - co.WithData(mat.PictureData{ - BackgroundColor: optional.Value(ui.Transparent()), - Image: co.OpenImage(scope, "ui/images/logo.png"), - Mode: mat.ImageModeFit, - }) - co.WithLayoutData(mat.LayoutData{ - Width: optional.Value(512), - Height: optional.Value(128), - HorizontalCenter: optional.Value(0), - VerticalCenter: optional.Value(0), - }) - })) - }) -}) diff --git a/internal/ui/model/application.go b/internal/ui/model/application.go index 46848a1..37eea90 100644 --- a/internal/ui/model/application.go +++ b/internal/ui/model/application.go @@ -1,51 +1,58 @@ package model -import ( - "github.com/mokiat/lacking/ui/mvc" - "github.com/mokiat/rally-mka/internal/scene" -) - -var ( - ChangeApplication = mvc.NewChange("application") - ChangeActiveView = mvc.SubChange(ChangeApplication, "active_view") - ChangeGameData = mvc.SubChange(ChangeApplication, "game_data") -) +import "github.com/mokiat/lacking/ui/mvc" const ( - ViewNameIntro ViewName = "intro" - ViewNameHome ViewName = "home" - ViewNamePlay ViewName = "play" + ViewNameIntro ViewName = "intro" + ViewNameHome ViewName = "home" + ViewNamePlay ViewName = "play" + ViewNameLoading ViewName = "loading" + ViewNameLicenses ViewName = "licenses" + ViewNameCredits ViewName = "credits" ) -type ViewName string +type ViewName = string + +var ( + ApplicationChange = mvc.NewChange("application") + ApplicationActiveViewChange = mvc.SubChange(ApplicationChange, "active_view") +) func NewApplication() *Application { return &Application{ Observable: mvc.NewObservable(), + loading: newLoading(), + home: newHome(), + play: newPlay(), activeView: ViewNameIntro, } } type Application struct { mvc.Observable + loading *Loading + home *Home + play *Play activeView ViewName - gameData *scene.Data } -func (a *Application) ActiveView() ViewName { - return a.activeView +func (a *Application) Loading() *Loading { + return a.loading } -func (a *Application) SetActiveView(view ViewName) { - a.activeView = view - a.SignalChange(ChangeActiveView) +func (a *Application) Home() *Home { + return a.home +} + +func (a *Application) Play() *Play { + return a.play } -func (a *Application) GameData() *scene.Data { - return a.gameData +func (a *Application) ActiveView() ViewName { + return a.activeView } -func (a *Application) SetGameData(data *scene.Data) { - a.gameData = data - a.SignalChange(ChangeGameData) +func (a *Application) SetActiveView(view ViewName) { + a.activeView = view + a.SignalChange(ApplicationActiveViewChange) } diff --git a/internal/ui/model/home.go b/internal/ui/model/home.go new file mode 100644 index 0000000..7f64fdc --- /dev/null +++ b/internal/ui/model/home.go @@ -0,0 +1,81 @@ +package model + +import ( + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/game/graphics" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/game/data" +) + +var ( + HomeChange = mvc.NewChange("home") + HomeDataChange = mvc.SubChange(HomeChange, "data") + HomeSceneChange = mvc.SubChange(HomeChange, "scene") + HomeControllerChange = mvc.SubChange(HomeChange, "controller") + HomeEnvironmentChange = mvc.SubChange(HomeChange, "environment") +) + +type HomeScene struct { + Scene *game.Scene + + DaySkyColor sprec.Vec3 + DayAmbientLight *graphics.AmbientLight + DayDirectionalLight *graphics.DirectionalLight + + NightSkyColor sprec.Vec3 + NightAmbientLight *graphics.AmbientLight + NightSpotLight *graphics.SpotLight +} + +func newHome() *Home { + return &Home{ + Observable: mvc.NewObservable(), + controller: data.ControllerKeyboard, + environment: data.EnvironmentDay, + } +} + +type Home struct { + mvc.Observable + sceneData game.Promise[*data.HomeData] + scene *HomeScene + controller data.Controller + environment data.Environment +} + +func (h *Home) Data() game.Promise[*data.HomeData] { + return h.sceneData +} + +func (h *Home) SetData(sceneData game.Promise[*data.HomeData]) { + h.sceneData = sceneData + h.SignalChange(HomeDataChange) +} + +func (h *Home) Scene() *HomeScene { + return h.scene +} + +func (h *Home) SetScene(scene *HomeScene) { + h.scene = scene + h.SignalChange(HomeSceneChange) +} + +func (h *Home) Controller() data.Controller { + return h.controller +} + +func (h *Home) SetController(controller data.Controller) { + h.controller = controller + h.SignalChange(HomeControllerChange) +} + +func (h *Home) Environment() data.Environment { + return h.environment +} + +func (h *Home) SetEnvironment(environment data.Environment) { + h.environment = environment + h.SignalChange(HomeEnvironmentChange) +} diff --git a/internal/ui/model/loading.go b/internal/ui/model/loading.go new file mode 100644 index 0000000..f115e3a --- /dev/null +++ b/internal/ui/model/loading.go @@ -0,0 +1,45 @@ +package model + +import "github.com/mokiat/lacking/ui/mvc" + +var ( + LoadingChange = mvc.NewChange("loading") + LoadingPromiseChange = mvc.SubChange(LoadingChange, "promise") + LoadingNextViewChange = mvc.SubChange(LoadingChange, "next_view") +) + +type LoadingPromise interface { + OnReady(func()) +} + +func newLoading() *Loading { + return &Loading{ + Observable: mvc.NewObservable(), + promise: nil, + nextViewName: ViewNameIntro, + } +} + +type Loading struct { + mvc.Observable + promise LoadingPromise + nextViewName ViewName +} + +func (l *Loading) Promise() LoadingPromise { + return l.promise +} + +func (l *Loading) SetPromise(promise LoadingPromise) { + l.promise = promise + l.SignalChange(LoadingPromiseChange) +} + +func (l *Loading) NextViewName() ViewName { + return l.nextViewName +} + +func (l *Loading) SetNextViewName(name ViewName) { + l.nextViewName = name + l.SignalChange(LoadingNextViewChange) +} diff --git a/internal/ui/model/play.go b/internal/ui/model/play.go new file mode 100644 index 0000000..32e9fa7 --- /dev/null +++ b/internal/ui/model/play.go @@ -0,0 +1,32 @@ +package model + +import ( + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/game/data" +) + +var ( + PlayChange = mvc.NewChange("play") + PlayDataChange = mvc.SubChange(PlayChange, "data") +) + +func newPlay() *Play { + return &Play{ + Observable: mvc.NewObservable(), + } +} + +type Play struct { + mvc.Observable + sceneData game.Promise[*data.PlayData] +} + +func (h *Play) Data() game.Promise[*data.PlayData] { + return h.sceneData +} + +func (h *Play) SetData(sceneData game.Promise[*data.PlayData]) { + h.sceneData = sceneData + h.SignalChange(PlayDataChange) +} diff --git a/internal/ui/play/view.go b/internal/ui/play/view.go deleted file mode 100644 index fe4498f..0000000 --- a/internal/ui/play/view.go +++ /dev/null @@ -1,404 +0,0 @@ -package play - -import ( - "fmt" - - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/lacking/game/ecs" - "github.com/mokiat/lacking/game/graphics" - "github.com/mokiat/lacking/game/physics" - "github.com/mokiat/lacking/game/physics/solver" - "github.com/mokiat/lacking/resource" - "github.com/mokiat/lacking/ui" - co "github.com/mokiat/lacking/ui/component" - "github.com/mokiat/lacking/ui/mat" - "github.com/mokiat/lacking/util/optional" - "github.com/mokiat/rally-mka/internal/ecscomp" - "github.com/mokiat/rally-mka/internal/ecssys" - "github.com/mokiat/rally-mka/internal/game" - "github.com/mokiat/rally-mka/internal/global" - "github.com/mokiat/rally-mka/internal/scene" - "github.com/mokiat/rally-mka/internal/scene/car" -) - -const ( - correction = float32(0.9) - - anchorDistance = 6.0 - cameraDistance = 12.0 * correction - - carMaxSteeringAngle = 30 - carFrontAcceleration = 145 * 1 - carRearAcceleration = 160 * 1 - - // FIXME: Currently, too much front brakes cause the car - // to straighten. This is due to there being more pressure - // on the outer wheel which causes it to brake more and turn - // the car to neutral orientation. - carFrontDeceleration = 250 - carRearDeceleration = 180 - - suspensionEnabled = true - suspensionStart = float32(-0.25) * correction - suspensionEnd = float32(-0.6) * correction - suspensionWidth = float32(1.0) * correction - suspensionLength = float32(0.15) * correction - suspensionFrequencyHz = float32(4.0) - suspensionDampingRatio = float32(1.2) -) - -type ViewData struct { - GameData *scene.Data -} - -var View = co.Define(func(props co.Properties, scope co.Scope) co.Instance { - var ( - context = co.GetContext[global.Context]() - data = co.GetData[ViewData](props) - ) - - lifecycle := co.UseState(func() *playLifecycle { - return &playLifecycle{ - gameController: context.GameController, - gameData: data.GameData, - } - }).Get() - - speedState := co.UseState(func() float32 { - return float32(0.0) - }) - speed := speedState.Get() - - co.Once(func() { - co.Window(scope).SetCursorVisible(false) - }) - - co.Once(func() { - context.GameController.OnUpdate = func() { - carSpeed := ecscomp.GetPhysics(lifecycle.car).Body.Velocity().Length() * 3.6 - speedState.Set(carSpeed) - } - }) - - co.Defer(func() { - co.Window(scope).SetCursorVisible(true) - }) - - co.Once(func() { - lifecycle.init() - }) - - co.Defer(func() { - lifecycle.destroy() - }) - - return co.New(mat.Container, func() { - co.WithData(mat.ContainerData{ - Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), - }) - - co.WithChild("speed-label", co.New(mat.Label, func() { - co.WithData(mat.LabelData{ - Font: co.OpenFont(scope, "mat:///roboto-bold.ttf"), - FontSize: optional.Value(float32(24.0)), - FontColor: optional.Value(ui.White()), - Text: fmt.Sprintf("speed: %.4f", speed), - }) - co.WithLayoutData(mat.LayoutData{ - Left: optional.Value(0), - Top: optional.Value(0), - Width: optional.Value(200), - Height: optional.Value(24), - }) - })) - }) -}) - -type playLifecycle struct { - gameController *game.Controller - gameData *scene.Data - - gfxScene *graphics.Scene - physicsScene *physics.Scene - ecsScene *ecs.Scene - - renderSystem *ecssys.Renderer - vehicleSystem *ecssys.VehicleSystem - cameraStandSystem *ecssys.CameraStandSystem - - camera *graphics.Camera - car *ecs.Entity -} - -func (h *playLifecycle) init() { - h.gfxScene = h.gameController.GFXScene() - h.physicsScene = h.gameController.PhysicsScene() - h.ecsScene = h.gameController.ECSScene() - - h.renderSystem = h.gameController.RenderSystem() - h.vehicleSystem = h.gameController.VehicleSystem() - h.cameraStandSystem = h.gameController.CameraStandSystem() - - h.camera = h.gameController.Camera() - h.camera.SetPosition(sprec.NewVec3(0.0, 0.0, 0.0)) - h.camera.SetFoVMode(graphics.FoVModeHorizontalPlus) - h.camera.SetFoV(sprec.Degrees(66)) - h.camera.SetAutoExposure(true) - h.camera.SetExposure(1.0) - h.camera.SetAutoFocus(false) - - h.setupLevel(h.gameData.Level) -} - -func (h *playLifecycle) destroy() { - h.gameData.Dismiss() -} - -func (h *playLifecycle) setupLevel(level *resource.Level) { - h.gfxScene.Sky().SetBackgroundColor(sprec.NewVec3(0.0, 0.3, 0.8)) - h.gfxScene.Sky().SetSkybox(level.SkyboxTexture.GFXTexture) - - ambientLight := h.gfxScene.CreateAmbientLight() - ambientLight.SetReflectionTexture(level.AmbientReflectionTexture.GFXTexture) - ambientLight.SetRefractionTexture(level.AmbientRefractionTexture.GFXTexture) - - sunLight := h.gfxScene.CreateDirectionalLight() - sunLight.SetRotation(sprec.QuatProd( - sprec.RotationQuat(sprec.Degrees(225), sprec.BasisYVec3()), - sprec.RotationQuat(sprec.Degrees(-45), sprec.BasisXVec3()), - )) - sunLight.SetIntensity(sprec.NewVec3(1.2, 1.2, 1.2)) - - for _, staticMesh := range level.StaticMeshes { - h.gfxScene.CreateMesh(staticMesh.GFXMeshTemplate) - } - - for _, collisionMesh := range level.CollisionMeshes { - body := h.physicsScene.CreateBody() - body.SetPosition(sprec.ZeroVec3()) - body.SetOrientation(sprec.IdentityQuat()) - body.SetStatic(true) - body.SetRestitutionCoefficient(1.0) - body.SetCollisionShapes([]physics.CollisionShape{collisionMesh}) - } - - var createModelMesh func(matrix sprec.Mat4, node *resource.Node) - createModelMesh = func(matrix sprec.Mat4, node *resource.Node) { - modelMatrix := sprec.Mat4Prod(matrix, node.Matrix) - - if node.Mesh != nil { - gfxMesh := h.gfxScene.CreateMesh(node.Mesh.GFXMeshTemplate) - gfxMesh.SetPosition(modelMatrix.Translation()) - // TODO: SetRotation - // TODO: SetScale - } - - for _, child := range node.Children { - createModelMesh(modelMatrix, child) - } - } - - for _, staticEntity := range level.StaticEntities { - for _, node := range staticEntity.Model.Nodes { - createModelMesh(staticEntity.Matrix, node) - } - } - - carModel := h.gameData.CarModel - targetEntity := h.setupCarDemo(carModel, sprec.NewVec3(0.0, 3.0, 0.0)) - standTarget := targetEntity - standEntity := h.ecsScene.CreateEntity() - ecscomp.SetCameraStand(standEntity, &ecscomp.CameraStand{ - Target: standTarget, - Camera: h.camera, - AnchorPosition: sprec.Vec3Sum(ecscomp.GetPhysics(standTarget).Body.Position(), sprec.NewVec3(0.0, 0.0, -cameraDistance)), - AnchorDistance: anchorDistance, - CameraDistance: cameraDistance, - }) - h.car = targetEntity -} - -func (h *playLifecycle) setupCarDemo(model *resource.Model, position sprec.Vec3) *ecs.Entity { - chasis := car.Chassis(model). - WithName("chasis"). - WithPosition(position). - Build(h.ecsScene, h.gfxScene, h.physicsScene) - chasisPhysics := ecscomp.GetPhysics(chasis) - - flWheelRelativePosition := sprec.NewVec3(suspensionWidth, suspensionStart-suspensionLength, 1.07*correction) - flWheel := car.Wheel(model, car.FrontLeftWheelLocation). - WithName("front-left-wheel"). - WithPosition(sprec.Vec3Sum(position, flWheelRelativePosition)). - Build(h.ecsScene, h.gfxScene, h.physicsScene) - flWheelPhysics := ecscomp.GetPhysics(flWheel) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, flWheelPhysics.Body, - solver.NewMatchTranslation(). - SetPrimaryAnchor(flWheelRelativePosition). - SetIgnoreY(suspensionEnabled), - ) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, flWheelPhysics.Body, &solver.LimitTranslation{ - MaxY: suspensionStart, - MinY: suspensionEnd, - }) - flRotation := solver.NewMatchAxis(). - SetPrimaryAxis(sprec.BasisXVec3()). - SetSecondaryAxis(sprec.BasisXVec3()) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, flWheelPhysics.Body, flRotation) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, flWheelPhysics.Body, &solver.Coilover{ - PrimaryAnchor: flWheelRelativePosition, - FrequencyHz: suspensionFrequencyHz, - DampingRatio: suspensionDampingRatio, - }) - - frWheelRelativePosition := sprec.NewVec3(-suspensionWidth, suspensionStart-suspensionLength, 1.07*correction) - frWheel := car.Wheel(model, car.FrontRightWheelLocation). - WithName("front-right-wheel"). - WithPosition(sprec.Vec3Sum(position, frWheelRelativePosition)). - Build(h.ecsScene, h.gfxScene, h.physicsScene) - frWheelPhysics := ecscomp.GetPhysics(frWheel) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, frWheelPhysics.Body, - solver.NewMatchTranslation(). - SetPrimaryAnchor(frWheelRelativePosition). - SetIgnoreY(suspensionEnabled), - ) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, frWheelPhysics.Body, &solver.LimitTranslation{ - MaxY: suspensionStart, - MinY: suspensionEnd, - }) - frRotation := solver.NewMatchAxis(). - SetPrimaryAxis(sprec.BasisXVec3()). - SetSecondaryAxis(sprec.BasisXVec3()) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, frWheelPhysics.Body, frRotation) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, frWheelPhysics.Body, &solver.Coilover{ - PrimaryAnchor: frWheelRelativePosition, - FrequencyHz: suspensionFrequencyHz, - DampingRatio: suspensionDampingRatio, - }) - - h.physicsScene.CreateDoubleBodyConstraint(flWheelPhysics.Body, frWheelPhysics.Body, &Differential{}) - - blWheelRelativePosition := sprec.NewVec3(suspensionWidth, suspensionStart-suspensionLength, -1.56*correction) - blWheel := car.Wheel(model, car.BackLeftWheelLocation). - WithName("back-left-wheel"). - WithPosition(sprec.Vec3Sum(position, blWheelRelativePosition)). - Build(h.ecsScene, h.gfxScene, h.physicsScene) - blWheelPhysics := ecscomp.GetPhysics(blWheel) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, blWheelPhysics.Body, - solver.NewMatchTranslation(). - SetPrimaryAnchor(blWheelRelativePosition). - SetIgnoreY(suspensionEnabled), - ) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, blWheelPhysics.Body, &solver.LimitTranslation{ - MaxY: suspensionStart, - MinY: suspensionEnd, - }) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, blWheelPhysics.Body, - solver.NewMatchAxis(). - SetPrimaryAxis(sprec.BasisXVec3()). - SetSecondaryAxis(sprec.BasisXVec3()), - ) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, blWheelPhysics.Body, &solver.Coilover{ - PrimaryAnchor: blWheelRelativePosition, - FrequencyHz: suspensionFrequencyHz, - DampingRatio: suspensionDampingRatio, - }) - - brWheelRelativePosition := sprec.NewVec3(-suspensionWidth, suspensionStart-suspensionLength, -1.56*correction) - brWheel := car.Wheel(model, car.BackRightWheelLocation). - WithName("back-right-wheel"). - WithPosition(sprec.Vec3Sum(position, brWheelRelativePosition)). - Build(h.ecsScene, h.gfxScene, h.physicsScene) - brWheelPhysics := ecscomp.GetPhysics(brWheel) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, brWheelPhysics.Body, - solver.NewMatchTranslation(). - SetPrimaryAnchor(brWheelRelativePosition). - SetIgnoreY(suspensionEnabled), - ) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, brWheelPhysics.Body, &solver.LimitTranslation{ - MaxY: suspensionStart, - MinY: suspensionEnd, - }) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, brWheelPhysics.Body, solver.NewMatchAxis(). - SetPrimaryAxis(sprec.BasisXVec3()). - SetSecondaryAxis(sprec.BasisXVec3()), - ) - h.physicsScene.CreateDoubleBodyConstraint(chasisPhysics.Body, brWheelPhysics.Body, &solver.Coilover{ - PrimaryAnchor: brWheelRelativePosition, - FrequencyHz: suspensionFrequencyHz, - DampingRatio: suspensionDampingRatio, - }) - - h.physicsScene.CreateDoubleBodyConstraint(blWheelPhysics.Body, brWheelPhysics.Body, &Differential{}) - - car := h.ecsScene.CreateEntity() - ecscomp.SetVehicle(car, &ecscomp.Vehicle{ - MaxSteeringAngle: sprec.Degrees(carMaxSteeringAngle), - SteeringAngle: sprec.Degrees(0.0), - Acceleration: 0.0, - Deceleration: 0.0, - Chassis: &ecscomp.Chassis{ - Body: chasisPhysics.Body, - }, - Wheels: []*ecscomp.Wheel{ - { - Body: flWheelPhysics.Body, - RotationConstraint: flRotation, - AccelerationVelocity: carFrontAcceleration, - DecelerationVelocity: carFrontDeceleration, - }, - { - Body: frWheelPhysics.Body, - RotationConstraint: frRotation, - AccelerationVelocity: carFrontAcceleration, - DecelerationVelocity: carFrontDeceleration, - }, - { - Body: blWheelPhysics.Body, - AccelerationVelocity: carRearAcceleration, - DecelerationVelocity: carRearDeceleration, - }, - { - Body: brWheelPhysics.Body, - AccelerationVelocity: carRearAcceleration, - DecelerationVelocity: carRearDeceleration, - }, - }, - }) - ecscomp.SetPlayerControl(car, &ecscomp.PlayerControl{}) - - return chasis -} - -var _ physics.DBConstraintSolver = (*Differential)(nil) - -type Differential struct { - physics.NilDBConstraintSolver -} - -func (d *Differential) CalculateImpulses(ctx physics.DBSolverContext) physics.DBImpulseSolution { - firstRotation := sprec.Vec3Dot(ctx.Primary.Orientation().OrientationX(), ctx.Primary.AngularVelocity()) - secondRotation := sprec.Vec3Dot(ctx.Secondary.Orientation().OrientationX(), ctx.Secondary.AngularVelocity()) - - const maxDelta = float32(100.0) - - var firstCorrection sprec.Vec3 - if firstRotation > secondRotation+maxDelta { - firstCorrection = sprec.Vec3Prod(ctx.Primary.Orientation().OrientationX(), secondRotation+maxDelta-firstRotation) - } - - var secondCorrection sprec.Vec3 - if secondRotation > firstRotation+maxDelta { - secondCorrection = sprec.Vec3Prod(ctx.Secondary.Orientation().OrientationX(), firstRotation+maxDelta-secondRotation) - } - - return physics.DBImpulseSolution{ - Primary: physics.SBImpulseSolution{ - Impulse: sprec.ZeroVec3(), - AngularImpulse: firstCorrection, - }, - Secondary: physics.SBImpulseSolution{ - Impulse: sprec.ZeroVec3(), - AngularImpulse: secondCorrection, - }, - } -} diff --git a/internal/ui/theme/colors.go b/internal/ui/theme/colors.go new file mode 100644 index 0000000..3c406ef --- /dev/null +++ b/internal/ui/theme/colors.go @@ -0,0 +1,10 @@ +package theme + +import "github.com/mokiat/lacking/ui" + +var ( + PrimaryColor = ui.White() + PrimaryOverColor = ui.RGB(0x00, 0x8a, 0xfb) + PrimaryDownColor = ui.RGB(0x00, 0x49, 0x7f) + PrimaryDisabledColor = ui.RGB(0x75, 0x75, 0x75) +) diff --git a/internal/ui/view/application.go b/internal/ui/view/application.go index 4556f4e..44cde63 100644 --- a/internal/ui/view/application.go +++ b/internal/ui/view/application.go @@ -4,10 +4,7 @@ import ( co "github.com/mokiat/lacking/ui/component" "github.com/mokiat/lacking/ui/mat" "github.com/mokiat/lacking/ui/mvc" - "github.com/mokiat/rally-mka/internal/ui/home" - "github.com/mokiat/rally-mka/internal/ui/intro" "github.com/mokiat/rally-mka/internal/ui/model" - "github.com/mokiat/rally-mka/internal/ui/play" ) var Application = co.Define(func(props co.Properties, scope co.Scope) co.Instance { @@ -16,21 +13,38 @@ var Application = co.Define(func(props co.Properties, scope co.Scope) co.Instanc ) mvc.UseBinding(data, func(ch mvc.Change) bool { - return mvc.IsChange(ch, model.ChangeActiveView) + return mvc.IsChange(ch, model.ApplicationActiveViewChange) }) return co.New(mat.Switch, func() { co.WithData(mat.SwitchData{ - ChildKey: string(data.ActiveView()), + ChildKey: data.ActiveView(), }) - co.WithScope(scope) - co.WithChild(string(model.ViewNameIntro), co.New(intro.View, func() {})) - co.WithChild(string(model.ViewNameHome), co.New(home.View, func() {})) - co.WithChild(string(model.ViewNamePlay), co.New(play.View, func() { - co.WithData(play.ViewData{ - GameData: data.GameData(), + co.WithChild(model.ViewNameIntro, co.New(IntroScreen, func() { + co.WithData(IntroScreenData{ + Home: data.Home(), + LoadingModel: data.Loading(), }) })) + co.WithChild(model.ViewNameLoading, co.New(LoadingScreen, func() { + co.WithData(LoadingScreenData{ + Model: data.Loading(), + }) + })) + co.WithChild(model.ViewNameHome, co.New(HomeScreen, func() { + co.WithData(HomeScreenData{ + Loading: data.Loading(), + Home: data.Home(), + Play: data.Play(), + }) + })) + co.WithChild(model.ViewNamePlay, co.New(PlayScreen, func() { + co.WithData(PlayScreenData{ + Play: data.Play(), + }) + })) + co.WithChild(model.ViewNameLicenses, co.New(LicensesScreen, nil)) + co.WithChild(model.ViewNameCredits, co.New(CreditsScreen, nil)) }) }) diff --git a/internal/ui/view/credits_screen.go b/internal/ui/view/credits_screen.go new file mode 100644 index 0000000..158b3ca --- /dev/null +++ b/internal/ui/view/credits_screen.go @@ -0,0 +1,247 @@ +package view + +import ( + "fmt" + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/ui/action" + "github.com/mokiat/rally-mka/internal/ui/model" + "github.com/mokiat/rally-mka/internal/ui/theme" + "github.com/mokiat/rally-mka/internal/ui/widget" +) + +var ( + sections []creditsSection +) + +func init() { + sections = append(sections, createSection("ART & PROGRAMMING", + "Momchil Atanasov", + )) + sections = append(sections, createSection("NOTABLE TOOLING", + "Visual Studio Code", + "Blender", + "Affinity Designer", + "Procreate", + "GIMP", + )) + sections = append(sections, createSection("SPECIAL THANKS", + "Go Developers for the brilliant programming language", + "\"GameDev БГ\" Discord server for provided support", + "Open-source developers for used libraries and tools", + "Grant Abbitt for video tutorials", + "Erin Catto for articles and videos", + )) +} + +var CreditsScreen = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + fadeInVisible := co.UseState(func() bool { + return true + }) + + fadeOutVisible := co.UseState(func() bool { + return false + }) + + onBackClicked := func() { + mvc.Dispatch(scope, action.ChangeView{ + ViewName: model.ViewNameHome, + }) + } + + onCreditsFinished := func() { + fadeOutVisible.Set(true) + } + + onFadeInFinished := func() { + fadeInVisible.Set(false) + } + + onFadeOutFinished := func() { + onBackClicked() + } + + return co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.Black()), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithChild("menu-pane", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.Black()), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(0), + Width: opt.V(200), + }) + + co.WithChild("button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Back", + }) + co.WithLayoutData(mat.LayoutData{ + HorizontalCenter: opt.V(0), + Bottom: opt.V(100), + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: onBackClicked, + }) + })) + })) + + co.WithChild("content-pane", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.RGB(0x11, 0x11, 0x11)), + Layout: mat.NewFillLayout(), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(200), + Right: opt.V(0), + }) + + co.WithChild("scroll-pane", co.New(widget.AutoScroll, func() { + co.WithData(widget.AutoScrollData{ + Velocity: 50.0, + }) + co.WithCallbackData(widget.AutoScrollCallbackData{ + OnFinished: onCreditsFinished, + }) + + co.WithChild("credits-list", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewVerticalLayout(mat.VerticalLayoutSettings{ + ContentAlignment: mat.AlignmentCenter, + ContentSpacing: 20, + }), + }) + co.WithLayoutData(mat.LayoutData{ + GrowHorizontally: true, + }) + + co.WithChild("header-spacing", co.New(mat.Spacing, func() { + co.WithData(mat.SpacingData{ + Width: 10, + Height: 500, + }) + })) + + co.WithChild("logo-picture", co.New(mat.Picture, func() { + co.WithData(mat.PictureData{ + BackgroundColor: opt.V(ui.Transparent()), + Image: co.OpenImage(scope, "ui/images/logo.png"), + Mode: mat.ImageModeFit, + }) + co.WithLayoutData(mat.LayoutData{ + Width: opt.V(512), + Height: opt.V(128), + HorizontalCenter: opt.V(0), + VerticalCenter: opt.V(0), + }) + })) + + co.WithChild("section-spacing", co.New(mat.Spacing, func() { + co.WithData(mat.SpacingData{ + Width: 10, + Height: 100, + }) + })) + + for i, section := range sections { + co.WithChild(fmt.Sprintf("section-%d-title", i), co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-bold.ttf"), + FontSize: opt.V(float32(24)), + FontColor: opt.V(theme.PrimaryColor), + Text: section.Title, + }) + })) + for j, item := range section.Items { + co.WithChild(fmt.Sprintf("section-%d-item-%d", i, j), co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-regular.ttf"), + FontSize: opt.V(float32(32)), + FontColor: opt.V(theme.PrimaryOverColor), + Text: item, + }) + })) + } + co.WithChild(fmt.Sprintf("post-section-%d-spacing", i), co.New(mat.Spacing, func() { + co.WithData(mat.SpacingData{ + Width: 10, + Height: 20, + }) + })) + } + + co.WithChild("thank-you-spacing", co.New(mat.Spacing, func() { + co.WithData(mat.SpacingData{ + Width: 10, + Height: 300, + }) + })) + + co.WithChild("thank-you", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-bold.ttf"), + FontSize: opt.V(float32(64.0)), + FontColor: opt.V(ui.White()), + Text: "THANK YOU", + }) + })) + + co.WithChild("footer-spacing", co.New(mat.Spacing, func() { + co.WithData(mat.SpacingData{ + Width: 10, + Height: 300, + }) + })) + })) + })) + + if fadeInVisible.Get() { + co.WithChild("fade-in", co.New(widget.FadeIn, func() { + co.WithData(widget.FadeInData{ + Duration: time.Second, + }) + co.WithCallbackData(widget.FadeInCallbackData{ + OnFinished: onFadeInFinished, + }) + })) + } + + if fadeOutVisible.Get() { + co.WithChild("fade-out", co.New(widget.FadeOut, func() { + co.WithData(widget.FadeOutData{ + Duration: time.Second, + }) + co.WithCallbackData(widget.FadeOutCallbackData{ + OnFinished: onFadeOutFinished, + }) + })) + } + })) + }) +}) + +func createSection(title string, items ...string) creditsSection { + return creditsSection{ + Title: title, + Items: items, + } +} + +type creditsSection struct { + Title string + Items []string +} diff --git a/internal/ui/view/exit_menu.go b/internal/ui/view/exit_menu.go new file mode 100644 index 0000000..83acbb2 --- /dev/null +++ b/internal/ui/view/exit_menu.go @@ -0,0 +1,106 @@ +package view + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/rally-mka/internal/ui/widget" +) + +var ExitMenu = co.DefineType(&ExitMenuPresenter{}) + +type ExitMenuCallback struct { + OnContinue func() + OnHome func() + OnExit func() +} + +type ExitMenuPresenter struct { + CallbackData ExitMenuCallback `co:"callback"` +} + +var _ ui.ElementKeyboardHandler = (*ExitMenuPresenter)(nil) + +func (p *ExitMenuPresenter) OnKeyboardEvent(element *ui.Element, event ui.KeyboardEvent) bool { + switch event.Code { + case ui.KeyCodeEscape: + if event.Type == ui.KeyboardEventTypeKeyUp { + p.CallbackData.OnContinue() + } + return true + default: + return false + } +} + +func (p *ExitMenuPresenter) Render() co.Instance { + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: p, + Focusable: opt.V(true), + Focused: opt.V(true), + Layout: mat.NewFillLayout(), + }) + + co.WithChild("background", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.RGBA(0x00, 0x00, 0x00, 0xAA)), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithChild("pane", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.RGBA(0, 0, 0, 192)), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(0), + Width: opt.V(320), + }) + + co.WithChild("holder", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewVerticalLayout(mat.VerticalLayoutSettings{ + ContentAlignment: mat.AlignmentLeft, + ContentSpacing: 15, + }), + }) + co.WithLayoutData(mat.LayoutData{ + Left: opt.V(75), + VerticalCenter: opt.V(0), + }) + + co.WithChild("continue-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Continue", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.CallbackData.OnContinue, + }) + })) + + co.WithChild("home-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Main Menu", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.CallbackData.OnHome, + }) + })) + + co.WithChild("exit-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Exit", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.CallbackData.OnExit, + }) + })) + })) + })) + })) + }) +} diff --git a/internal/ui/view/home_screen.go b/internal/ui/view/home_screen.go new file mode 100644 index 0000000..f21a80a --- /dev/null +++ b/internal/ui/view/home_screen.go @@ -0,0 +1,580 @@ +package view + +import ( + "github.com/mokiat/gblob" + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/game" + "github.com/mokiat/lacking/game/graphics" + "github.com/mokiat/lacking/log" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/game/data" + "github.com/mokiat/rally-mka/internal/ui/action" + "github.com/mokiat/rally-mka/internal/ui/global" + "github.com/mokiat/rally-mka/internal/ui/model" + "github.com/mokiat/rally-mka/internal/ui/widget" + "github.com/x448/float16" +) + +var HomeScreen = co.DefineType(&HomeScreenPresenter{}) + +type HomeScreenData struct { + Loading *model.Loading + Home *model.Home + Play *model.Play +} + +type HomeScreenPresenter struct { + Scope co.Scope `co:"scope"` + Data HomeScreenData `co:"data"` + Invalidate func() `co:"invalidate"` + + engine *game.Engine + resourceSet *game.ResourceSet + + loadingModel *model.Loading + homeModel *model.Home + playModel *model.Play + scene *model.HomeScene + + showOptions bool +} + +func (p *HomeScreenPresenter) OnCreate() { + var globalContext global.Context + co.InjectContext(&globalContext) + + p.engine = globalContext.Engine + p.resourceSet = globalContext.ResourceSet + p.loadingModel = p.Data.Loading + p.homeModel = p.Data.Home + p.playModel = p.Data.Play + + // TODO: Figure out an alternative way for TypeComponents + mvc.UseBinding(p.homeModel, func(ch mvc.Change) bool { + return mvc.IsChange(ch, model.HomeChange) + }) + + p.scene = p.homeModel.Scene() + if p.scene == nil { + p.scene = p.createScene() + p.homeModel.SetScene(p.scene) + p.onDayClicked() + } + p.engine.SetActiveScene(p.scene.Scene) +} + +func (p *HomeScreenPresenter) OnDelete() { + p.engine.SetActiveScene(nil) +} + +func (p *HomeScreenPresenter) Render() co.Instance { + controller := p.homeModel.Controller() + environment := p.homeModel.Environment() + + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithChild("pane", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.RGBA(0, 0, 0, 192)), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(0), + Width: opt.V(320), + }) + + co.WithChild("holder", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewVerticalLayout(mat.VerticalLayoutSettings{ + ContentAlignment: mat.AlignmentLeft, + ContentSpacing: 15, + }), + }) + co.WithLayoutData(mat.LayoutData{ + Left: opt.V(75), + VerticalCenter: opt.V(0), + }) + + if p.showOptions { + co.WithChild("start-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Start", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.onStartClicked, + }) + })) + + co.WithChild("back-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Back", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.onBackClicked, + }) + })) + } else { + co.WithChild("play-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Play", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.onPlayClicked, + }) + })) + + co.WithChild("licenses-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Licenses", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.onLicensesClicked, + }) + })) + + co.WithChild("credits-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Credits", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.onCreditsClicked, + }) + })) + + co.WithChild("exit-button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Exit", + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: p.onExitClicked, + }) + })) + } + })) + })) + + if p.showOptions { + co.WithChild("options", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.RGBA(0, 0, 0, 128)), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(320), + Right: opt.V(0), + }) + + co.WithChild("options-pane", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewVerticalLayout(mat.VerticalLayoutSettings{ + ContentAlignment: mat.AlignmentCenter, + ContentSpacing: 20, + }), + }) + co.WithLayoutData(mat.LayoutData{ + HorizontalCenter: opt.V(0), + VerticalCenter: opt.V(0), + }) + + co.WithChild("controller-toggles", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewHorizontalLayout(mat.HorizontalLayoutSettings{ + ContentAlignment: mat.AlignmentCenter, + ContentSpacing: 40, + }), + }) + + co.WithChild("keyboard", co.New(widget.Toggle, func() { + co.WithData(widget.ToggleData{ + Text: "Keyboard", + Selected: controller == data.ControllerKeyboard, + }) + co.WithCallbackData(widget.ToggleCallbackData{ + ClickListener: p.onKeyboardClicked, + }) + })) + + co.WithChild("mouse", co.New(widget.Toggle, func() { + co.WithData(widget.ToggleData{ + Text: "Mouse", + Selected: controller == data.ControllerMouse, + }) + co.WithCallbackData(widget.ToggleCallbackData{ + ClickListener: p.onMouseClicked, + }) + })) + + co.WithChild("gamepad", co.New(widget.Toggle, func() { + co.WithData(widget.ToggleData{ + Text: "Gamepad", + Selected: controller == data.ControllerGamepad, + }) + co.WithCallbackData(widget.ToggleCallbackData{ + ClickListener: p.onGamepadClicked, + }) + })) + })) + + var imageURL string + switch controller { + case data.ControllerKeyboard: + imageURL = "ui/images/keyboard.png" + case data.ControllerMouse: + imageURL = "ui/images/mouse.png" + case data.ControllerGamepad: + imageURL = "ui/images/gamepad.png" + } + + co.WithChild("controller-image", co.New(mat.Picture, func() { + co.WithData(mat.PictureData{ + BackgroundColor: opt.V(ui.RGBA(0x00, 0x00, 0x00, 0x9A)), + Image: co.OpenImage(p.Scope, imageURL), + ImageColor: opt.V(ui.White()), + Mode: mat.ImageModeStretch, + }) + co.WithLayoutData(mat.LayoutData{ + Width: opt.V(600), + Height: opt.V(300), + }) + })) + + co.WithChild("controller-text", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(p.Scope, "mat:///roboto-regular.ttf"), + FontSize: opt.V(float32(24.0)), + FontColor: opt.V(ui.White()), + TextAlignment: mat.AlignmentCenter, + Text: p.controllerDescription(controller), + }) + })) + + co.WithChild("separator", co.New(widget.Separator, func() { + co.WithLayoutData(mat.LayoutData{ + Width: opt.V(600), + Height: opt.V(4), + }) + })) + + co.WithChild("environment-toggles", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Layout: mat.NewHorizontalLayout(mat.HorizontalLayoutSettings{ + ContentAlignment: mat.AlignmentCenter, + ContentSpacing: 40, + }), + }) + + co.WithChild("day", co.New(widget.Toggle, func() { + co.WithData(widget.ToggleData{ + Text: "Day", + Selected: environment == data.EnvironmentDay, + }) + co.WithCallbackData(widget.ToggleCallbackData{ + ClickListener: p.onDayClicked, + }) + })) + + co.WithChild("night", co.New(widget.Toggle, func() { + co.WithData(widget.ToggleData{ + Text: "Night", + Selected: environment == data.EnvironmentNight, + }) + co.WithCallbackData(widget.ToggleCallbackData{ + ClickListener: p.onNightClicked, + }) + })) + })) + + co.WithChild("environment-text", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(p.Scope, "mat:///roboto-regular.ttf"), + FontSize: opt.V(float32(24.0)), + FontColor: opt.V(ui.White()), + TextAlignment: mat.AlignmentCenter, + Text: p.environmentDescription(environment), + }) + })) + })) + })) + } + }) +} + +func (p *HomeScreenPresenter) createScene() *model.HomeScene { + result := &model.HomeScene{} + + promise := p.homeModel.Data() + sceneData, err := promise.Get() + if err != nil { + log.Error("ERROR: %v", err) // TODO: Go to error screen + return nil + } + + scene := p.engine.CreateScene() + scene.Initialize(sceneData.Scene) + result.Scene = scene + + camera := p.createCamera(scene.Graphics()) + scene.Graphics().SetActiveCamera(camera) + + result.DaySkyColor = sprec.NewVec3(20.0, 25.0, 30.0) + result.DayAmbientLight = p.createDayAmbientLight(scene.Graphics()) + result.DayDirectionalLight = scene.Graphics().CreateDirectionalLight(graphics.DirectionalLightInfo{ + EmitColor: dprec.NewVec3(10, 10, 6), + EmitRange: 16000, // FIXME + }) + dayDirectionalLightNode := game.NewNode() + dayDirectionalLightNode.SetPosition(dprec.NewVec3(-100.0, 100.0, 0.0)) + dayDirectionalLightNode.SetRotation(dprec.QuatProd( + dprec.RotationQuat(dprec.Degrees(-90), dprec.BasisYVec3()), + dprec.RotationQuat(dprec.Degrees(-45), dprec.BasisXVec3()), + )) + dayDirectionalLightNode.UseTransformation(game.IgnoreParentRotation) + dayDirectionalLightNode.SetAttachable(result.DayDirectionalLight) + + result.NightSkyColor = sprec.NewVec3(0.01, 0.01, 0.01) + result.NightAmbientLight = p.createNightAmbientLight(scene.Graphics()) + result.NightSpotLight = scene.Graphics().CreateSpotLight(graphics.SpotLightInfo{ + EmitColor: dprec.NewVec3(5000.0, 5000.0, 7500.0), + EmitOuterConeAngle: dprec.Degrees(50), + EmitInnerConeAngle: dprec.Degrees(20), + EmitRange: 1000, + }) + nightSpotLightNode := game.NewNode() + nightSpotLightNode.SetPosition(dprec.NewVec3(0.0, 0.0, 0.0)) + nightSpotLightNode.SetRotation(dprec.RotationQuat(dprec.Degrees(0), dprec.BasisXVec3())) + nightSpotLightNode.SetAttachable(result.NightSpotLight) + + sceneModel := scene.FindModel("Content") + // TODO: Remove manual attachment, once this is configurable from + // the packing. + scene.Root().AppendChild(sceneModel.Root()) + + if cameraNode := scene.Root().FindNode("Camera"); cameraNode != nil { + cameraNode.SetAttachable(camera) + cameraNode.AppendChild(dayDirectionalLightNode) + cameraNode.AppendChild(nightSpotLightNode) + } + + const animationName = "Action" + if animation := sceneModel.FindAnimation(animationName); animation != nil { + playback := scene.PlayAnimation(animation) + playback.SetLoop(true) + playback.SetSpeed(0.3) + } + return result +} + +func (p *HomeScreenPresenter) createCamera(scene *graphics.Scene) *graphics.Camera { + result := scene.CreateCamera() + result.SetFoVMode(graphics.FoVModeHorizontalPlus) + result.SetFoV(sprec.Degrees(66)) + result.SetAutoExposure(false) + result.SetExposure(0.1) + result.SetAutoFocus(false) + result.SetAutoExposureSpeed(0.1) + return result +} + +func (p *HomeScreenPresenter) createDayAmbientLight(scene *graphics.Scene) *graphics.AmbientLight { + reflectionData := gblob.LittleEndianBlock(make([]byte, 4*2)) + reflectionData.SetUint16(0, float16.Fromfloat32(20.0).Bits()) + reflectionData.SetUint16(2, float16.Fromfloat32(25.0).Bits()) + reflectionData.SetUint16(4, float16.Fromfloat32(30.0).Bits()) + reflectionData.SetUint16(6, float16.Fromfloat32(1.0).Bits()) + + reflectionTexture := p.engine.Graphics().CreateCubeTexture(graphics.CubeTextureDefinition{ + Dimension: 1, + Filtering: graphics.FilterNearest, + InternalFormat: graphics.InternalFormatRGBA16F, + DataFormat: graphics.DataFormatRGBA16F, + GammaCorrection: false, + GenerateMipmaps: false, + FrontSideData: reflectionData, + BackSideData: reflectionData, + LeftSideData: reflectionData, + RightSideData: reflectionData, + TopSideData: reflectionData, + BottomSideData: reflectionData, + }) + + refractionTexture := p.engine.Graphics().CreateCubeTexture(graphics.CubeTextureDefinition{ + Dimension: 1, + Filtering: graphics.FilterNearest, + InternalFormat: graphics.InternalFormatRGBA16F, + DataFormat: graphics.DataFormatRGBA16F, + GammaCorrection: false, + GenerateMipmaps: false, + FrontSideData: reflectionData, + BackSideData: reflectionData, + LeftSideData: reflectionData, + RightSideData: reflectionData, + TopSideData: reflectionData, + BottomSideData: reflectionData, + }) + + return scene.CreateAmbientLight(graphics.AmbientLightInfo{ + ReflectionTexture: reflectionTexture, + RefractionTexture: refractionTexture, + Position: dprec.ZeroVec3(), + InnerRadius: 5000, + OuterRadius: 5000, + }) +} + +func (p *HomeScreenPresenter) createNightAmbientLight(scene *graphics.Scene) *graphics.AmbientLight { + reflectionData := gblob.LittleEndianBlock(make([]byte, 4*2)) + reflectionData.SetUint16(0, float16.Fromfloat32(0.1).Bits()) + reflectionData.SetUint16(2, float16.Fromfloat32(0.1).Bits()) + reflectionData.SetUint16(4, float16.Fromfloat32(0.1).Bits()) + reflectionData.SetUint16(6, float16.Fromfloat32(1.0).Bits()) + + reflectionTexture := p.engine.Graphics().CreateCubeTexture(graphics.CubeTextureDefinition{ + Dimension: 1, + Filtering: graphics.FilterNearest, + InternalFormat: graphics.InternalFormatRGBA16F, + DataFormat: graphics.DataFormatRGBA16F, + GammaCorrection: false, + GenerateMipmaps: false, + FrontSideData: reflectionData, + BackSideData: reflectionData, + LeftSideData: reflectionData, + RightSideData: reflectionData, + TopSideData: reflectionData, + BottomSideData: reflectionData, + }) + + refractionTexture := p.engine.Graphics().CreateCubeTexture(graphics.CubeTextureDefinition{ + Dimension: 1, + Filtering: graphics.FilterNearest, + InternalFormat: graphics.InternalFormatRGBA16F, + DataFormat: graphics.DataFormatRGBA16F, + GammaCorrection: false, + GenerateMipmaps: false, + FrontSideData: reflectionData, + BackSideData: reflectionData, + LeftSideData: reflectionData, + RightSideData: reflectionData, + TopSideData: reflectionData, + BottomSideData: reflectionData, + }) + + return scene.CreateAmbientLight(graphics.AmbientLightInfo{ + ReflectionTexture: reflectionTexture, + RefractionTexture: refractionTexture, + Position: dprec.ZeroVec3(), + InnerRadius: 5000, + OuterRadius: 5000, + }) +} + +func (p *HomeScreenPresenter) controllerDescription(controller data.Controller) string { + switch controller { + case data.ControllerKeyboard: + return "Keyboard: Uses assists. Provides an average challenge." + case data.ControllerMouse: + return "Mouse: Just point and drive. Good for a casual play." + case data.ControllerGamepad: + return "Gamepad: No assists. Requires significant skills to control." + default: + return "" + } +} + +func (p *HomeScreenPresenter) environmentDescription(environment data.Environment) string { + switch environment { + case data.EnvironmentDay: + return "Day: A good starting point to learn the track." + case data.EnvironmentNight: + return "Night: Can be relaxing if you already know the track." + default: + return "" + } +} + +func (p *HomeScreenPresenter) onKeyboardClicked() { + p.homeModel.SetController(data.ControllerKeyboard) +} + +func (p *HomeScreenPresenter) onMouseClicked() { + p.homeModel.SetController(data.ControllerMouse) +} + +func (p *HomeScreenPresenter) onGamepadClicked() { + p.homeModel.SetController(data.ControllerGamepad) +} + +func (p *HomeScreenPresenter) onDayClicked() { + p.homeModel.SetEnvironment(data.EnvironmentDay) + + // Disable night lighting + p.scene.NightAmbientLight.SetActive(false) + p.scene.NightSpotLight.SetActive(false) + + // Enable day lighting + p.scene.Scene.Graphics().Sky().SetBackgroundColor(p.scene.DaySkyColor) + p.scene.DayAmbientLight.SetActive(true) + p.scene.DayDirectionalLight.SetActive(true) +} + +func (p *HomeScreenPresenter) onNightClicked() { + p.homeModel.SetEnvironment(data.EnvironmentNight) + + // Disable day lighting + p.scene.DayAmbientLight.SetActive(false) + p.scene.DayDirectionalLight.SetActive(false) + + // Enable night lighting + p.scene.Scene.Graphics().Sky().SetBackgroundColor(p.scene.NightSkyColor) + p.scene.NightAmbientLight.SetActive(true) + p.scene.NightSpotLight.SetActive(true) +} + +func (p *HomeScreenPresenter) onStartClicked() { + promise := data.LoadPlayData(p.engine, p.resourceSet, p.homeModel.Environment(), p.homeModel.Controller()) + p.playModel.SetData(promise) + + p.loadingModel.SetPromise(promise) + p.loadingModel.SetNextViewName(model.ViewNamePlay) + mvc.Dispatch(p.Scope, action.ChangeView{ + ViewName: model.ViewNameLoading, + }) +} + +func (p *HomeScreenPresenter) onBackClicked() { + // TODO: Add a `Property` concept instead of manual Invalidate. + p.showOptions = false + p.Invalidate() +} + +func (p *HomeScreenPresenter) onPlayClicked() { + p.showOptions = true + p.Invalidate() +} + +func (p *HomeScreenPresenter) onLicensesClicked() { + mvc.Dispatch(p.Scope, action.ChangeView{ + ViewName: model.ViewNameLicenses, + }) +} + +func (p *HomeScreenPresenter) onCreditsClicked() { + mvc.Dispatch(p.Scope, action.ChangeView{ + ViewName: model.ViewNameCredits, + }) +} + +func (p *HomeScreenPresenter) onExitClicked() { + co.Window(p.Scope).Close() +} diff --git a/internal/ui/view/intro_screen.go b/internal/ui/view/intro_screen.go new file mode 100644 index 0000000..eea43d5 --- /dev/null +++ b/internal/ui/view/intro_screen.go @@ -0,0 +1,81 @@ +package view + +import ( + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/game/data" + "github.com/mokiat/rally-mka/internal/ui/action" + "github.com/mokiat/rally-mka/internal/ui/global" + "github.com/mokiat/rally-mka/internal/ui/model" +) + +type IntroScreenData struct { + Home *model.Home + LoadingModel *model.Loading +} + +var IntroScreen = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + globalContext = co.GetContext[global.Context]() + screenData = co.GetData[IntroScreenData](props) + + engine = globalContext.Engine + resourceSet = globalContext.ResourceSet + + homeModel = screenData.Home + loadingModel = screenData.LoadingModel + ) + + co.Once(func() { + co.Window(scope).SetCursorVisible(false) + }) + co.Defer(func() { + co.Window(scope).SetCursorVisible(true) + }) + + co.Once(func() { + homeModel.SetData(data.LoadHomeData(engine, resourceSet)) + }) + + co.After(time.Second, func() { + promise := homeModel.Data() + if promise.Ready() { + // TODO: Handle errors!!! + mvc.Dispatch(scope, action.ChangeView{ + ViewName: model.ViewNameHome, + }) + } else { + loadingModel.SetPromise(promise) + loadingModel.SetNextViewName(model.ViewNameHome) + mvc.Dispatch(scope, action.ChangeView{ + ViewName: model.ViewNameLoading, + }) + } + }) + + return co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.Black()), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithChild("logo-picture", co.New(mat.Picture, func() { + co.WithData(mat.PictureData{ + BackgroundColor: opt.V(ui.Transparent()), + Image: co.OpenImage(scope, "ui/images/logo.png"), + Mode: mat.ImageModeFit, + }) + co.WithLayoutData(mat.LayoutData{ + Width: opt.V(512), + Height: opt.V(128), + HorizontalCenter: opt.V(0), + VerticalCenter: opt.V(0), + }) + })) + }) +}) diff --git a/internal/ui/view/licenses_screen.go b/internal/ui/view/licenses_screen.go new file mode 100644 index 0000000..b71396b --- /dev/null +++ b/internal/ui/view/licenses_screen.go @@ -0,0 +1,137 @@ +package view + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/ui/action" + "github.com/mokiat/rally-mka/internal/ui/model" + "github.com/mokiat/rally-mka/internal/ui/widget" + "github.com/mokiat/rally-mka/resources" +) + +var LicensesScreen = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + onBackClicked := func() { + mvc.Dispatch(scope, action.ChangeView{ + ViewName: model.ViewNameHome, + }) + } + + return co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.Black()), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithChild("menu-pane", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.Black()), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(0), + Width: opt.V(200), + }) + + co.WithChild("button", co.New(widget.Button, func() { + co.WithData(widget.ButtonData{ + Text: "Back", + }) + co.WithLayoutData(mat.LayoutData{ + HorizontalCenter: opt.V(0), + Bottom: opt.V(100), + }) + co.WithCallbackData(widget.ButtonCallbackData{ + ClickListener: onBackClicked, + }) + })) + })) + + co.WithChild("content-pane", co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.RGB(0x11, 0x11, 0x11)), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Bottom: opt.V(0), + Left: opt.V(200), + Right: opt.V(0), + }) + + co.WithChild("title", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-bold.ttf"), + FontSize: opt.V(float32(32)), + FontColor: opt.V(ui.White()), + Text: "Open-Source Licenses", + }) + + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(15), + Height: opt.V(32), + HorizontalCenter: opt.V(0), + }) + })) + + co.WithChild("sub-title", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-italic.ttf"), + FontSize: opt.V(float32(20)), + FontColor: opt.V(ui.White()), + Text: "- scroll to view all -", + }) + + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(50), + Height: opt.V(20), + HorizontalCenter: opt.V(0), + }) + })) + + co.WithChild("license-scroll-pane", co.New(mat.ScrollPane, func() { + co.WithData(mat.ScrollPaneData{ + DisableHorizontal: true, + DisableVertical: false, + Focused: true, + }) + + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(80), + Bottom: opt.V(0), + Left: opt.V(0), + Right: opt.V(0), + }) + + co.WithChild("license-holder", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Padding: ui.Spacing{ + Top: 100, + Bottom: 100, + }, + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + co.WithLayoutData(mat.LayoutData{ + GrowHorizontally: true, + }) + co.WithChild("license-text", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-bold.ttf"), + FontSize: opt.V(float32(16)), + FontColor: opt.V(ui.White()), + Text: resources.Licenses, + }) + co.WithLayoutData(mat.LayoutData{ + HorizontalCenter: opt.V(0), + VerticalCenter: opt.V(0), + }) + })) + })) + })) + })) + }) +}) diff --git a/internal/ui/view/loading_screen.go b/internal/ui/view/loading_screen.go new file mode 100644 index 0000000..1234a4e --- /dev/null +++ b/internal/ui/view/loading_screen.go @@ -0,0 +1,61 @@ +package view + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/rally-mka/internal/ui/action" + "github.com/mokiat/rally-mka/internal/ui/model" + "github.com/mokiat/rally-mka/internal/ui/theme" + "github.com/mokiat/rally-mka/internal/ui/widget" +) + +type LoadingScreenData struct { + Model *model.Loading +} + +var LoadingScreen = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + screenData = co.GetData[LoadingScreenData](props) + loadingModel = screenData.Model + ) + + co.Once(func() { + loadingModel.Promise().OnReady(func() { + // TODO: Handle errors! + + mvc.Dispatch(scope, action.ChangeView{ + ViewName: loadingModel.NextViewName(), + }) + }) + }) + + return co.New(mat.Container, func() { + co.WithData(mat.ContainerData{ + BackgroundColor: opt.V(ui.Black()), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithChild("loading", co.New(widget.Loading, func() { + co.WithLayoutData(mat.LayoutData{ + HorizontalCenter: opt.V(0), + VerticalCenter: opt.V(0), + }) + })) + + co.WithChild("info-label", co.New(mat.Label, func() { + co.WithData(mat.LabelData{ + Font: co.OpenFont(scope, "mat:///roboto-italic.ttf"), + FontSize: opt.V(float32(32)), + FontColor: opt.V(theme.PrimaryColor), + Text: "Loading...", + }) + co.WithLayoutData(mat.LayoutData{ + Right: opt.V(40), + Bottom: opt.V(40), + }) + })) + }) +}) diff --git a/internal/ui/view/play_screen.go b/internal/ui/view/play_screen.go new file mode 100644 index 0000000..dda8657 --- /dev/null +++ b/internal/ui/view/play_screen.go @@ -0,0 +1,205 @@ +package view + +import ( + "fmt" + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/ui/mvc" + "github.com/mokiat/lacking/util/metrics" + "github.com/mokiat/rally-mka/internal/game/data" + "github.com/mokiat/rally-mka/internal/ui/action" + "github.com/mokiat/rally-mka/internal/ui/controller" + "github.com/mokiat/rally-mka/internal/ui/global" + "github.com/mokiat/rally-mka/internal/ui/model" + "github.com/mokiat/rally-mka/internal/ui/widget" +) + +var PlayScreen = co.DefineType(&PlayScreenPresenter{}) + +type PlayScreenData struct { + Play *model.Play +} + +type PlayScreenPresenter struct { + Scope co.Scope `co:"scope"` + Data PlayScreenData `co:"data"` + Invalidate func() `co:"invalidate"` + + hideCursor bool + controller *controller.PlayController + + debugVisible bool + debugRegions []metrics.RegionStat + debugRegionsTicker *time.Ticker + debugRegionsStop chan struct{} + + rootElement *ui.Element + exitMenu co.Overlay +} + +var _ ui.ElementKeyboardHandler = (*PlayScreenPresenter)(nil) +var _ ui.ElementMouseHandler = (*PlayScreenPresenter)(nil) + +func (p *PlayScreenPresenter) OnCreate() { + var context global.Context + co.InjectContext(&context) + + // FIXME: This is ugly and complicated. Come up with a better API + // than what Go provides that is integrated into component library and + // handles everything (cleanup, thread scheduling, etc). + p.debugRegionsTicker = time.NewTicker(time.Second) + p.debugRegionsStop = make(chan struct{}) + go func() { + for { + select { + case <-p.debugRegionsTicker.C: + co.Schedule(func() { + p.debugRegions = metrics.RegionStats() + p.Invalidate() + }) + case <-p.debugRegionsStop: + return + } + } + }() + + // FIXME: This may actually panic if there is a third party + // waiting / reading on this and it happens to match the Get call. + playData, err := p.Data.Play.Data().Get() + if err != nil { + panic(fmt.Errorf("failed to get data: %w", err)) + } + p.controller = controller.NewPlayController(co.Window(p.Scope).Window, context.Engine, playData) + p.controller.Start(playData.Environment, playData.Controller) + + p.hideCursor = playData.Controller != data.ControllerMouse + co.Window(p.Scope).SetCursorVisible(!p.hideCursor) +} + +func (p *PlayScreenPresenter) OnDelete() { + defer p.controller.Stop() + defer p.debugRegionsTicker.Stop() + defer close(p.debugRegionsStop) + defer co.Window(p.Scope).SetCursorVisible(true) +} + +func (p *PlayScreenPresenter) OnMouseEvent(element *ui.Element, event ui.MouseEvent) bool { + return p.controller.OnMouseEvent(element, event) +} + +func (p *PlayScreenPresenter) OnKeyboardEvent(element *ui.Element, event ui.KeyboardEvent) bool { + switch event.Code { + case ui.KeyCodeEscape: + if event.Type == ui.KeyboardEventTypeKeyUp { + p.controller.Pause() + co.Window(p.Scope).SetCursorVisible(true) + p.exitMenu = co.OpenOverlay(p.Scope, co.New(ExitMenu, func() { + co.WithCallbackData(ExitMenuCallback{ + OnContinue: p.onContinue, + OnHome: p.onGoHome, + OnExit: p.onExit, + }) + })) + } + return true + case ui.KeyCodeTab: + if event.Type == ui.KeyboardEventTypeKeyDown { + p.debugVisible = !p.debugVisible + p.Invalidate() + } + return true + case ui.KeyCodeEnter: + if event.Type == ui.KeyboardEventTypeKeyDown { + p.controller.ToggleCamera() + } + return true + default: + return p.controller.OnKeyboardEvent(event) + } +} + +func (p *PlayScreenPresenter) Render() co.Instance { + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Reference: &p.rootElement, + Essence: p, + Focusable: opt.V(true), + Focused: opt.V(true), + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + if p.debugVisible { + co.WithChild("regions", co.New(widget.RegionBlock, func() { + co.WithData(widget.RegionBlockData{ + Regions: p.debugRegions, + }) + co.WithLayoutData(mat.LayoutData{ + Top: opt.V(0), + Left: opt.V(0), + Right: opt.V(0), + }) + })) + } + + co.WithChild("dashboard", co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Padding: ui.Spacing{ + Left: 20, + Right: 20, + Bottom: 20, + }, + Layout: mat.NewAnchorLayout(mat.AnchorLayoutSettings{}), + }) + + co.WithLayoutData(mat.LayoutData{ + Left: opt.V(0), + Right: opt.V(0), + Bottom: opt.V(0), + }) + + co.WithChild("speedometer", co.New(widget.Speedometer, func() { + co.WithData(widget.SpeedometerData{ + Source: p.controller, + }) + + co.WithLayoutData(mat.LayoutData{ + Left: opt.V(20), + Bottom: opt.V(0), + }) + })) + + co.WithChild("gearshifter", co.New(widget.GearShifter, func() { + co.WithData(widget.GearShifterData{ + Source: p.controller, + }) + + co.WithLayoutData(mat.LayoutData{ + Right: opt.V(20), + Bottom: opt.V(0), + }) + })) + })) + }) +} + +func (p *PlayScreenPresenter) onContinue() { + p.exitMenu.Close() + p.controller.Resume() + co.Window(p.Scope).GrantFocus(p.rootElement) + co.Window(p.Scope).SetCursorVisible(!p.hideCursor) +} + +func (p *PlayScreenPresenter) onGoHome() { + p.exitMenu.Close() + mvc.Dispatch(p.Scope, action.ChangeView{ + ViewName: model.ViewNameHome, + }) +} + +func (p *PlayScreenPresenter) onExit() { + co.Window(p.Scope).Close() +} diff --git a/internal/ui/widget/autoscroll.go b/internal/ui/widget/autoscroll.go new file mode 100644 index 0000000..7bd15c5 --- /dev/null +++ b/internal/ui/widget/autoscroll.go @@ -0,0 +1,119 @@ +package widget + +import ( + "time" + + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +type AutoScrollData struct { + Velocity float64 +} + +var defaultAutoScrollData = AutoScrollData{ + Velocity: 50.0, +} + +type AutoScrollCallbackData struct { + OnFinished func() +} + +var defaultAutoScrollCallbackData = AutoScrollCallbackData{ + OnFinished: func() {}, +} + +var AutoScroll = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + data = co.GetOptionalData(props, defaultAutoScrollData) + callbackData = co.GetOptionalCallbackData(props, defaultAutoScrollCallbackData) + ) + + essence := co.UseState(func() *scrollPaneEssence { + return &scrollPaneEssence{ + lastTick: time.Now(), + } + }).Get() + essence.velocity = data.Velocity + essence.onFinished = callbackData.OnFinished + + return co.New(mat.Element, func() { + co.WithData(co.ElementData{ + Essence: essence, + Layout: essence, + }) + co.WithLayoutData(props.LayoutData()) + co.WithChildren(props.Children()) + }) +}) + +var _ ui.Layout = (*scrollPaneEssence)(nil) +var _ ui.ElementRenderHandler = (*scrollPaneEssence)(nil) + +type scrollPaneEssence struct { + offsetY float64 + maxOffsetY float64 + velocity float64 + lastTick time.Time + onFinished func() +} + +func (e *scrollPaneEssence) Apply(element *ui.Element) { + var maxChildSize ui.Size + + contentBounds := element.ContentBounds() + for childElement := element.FirstChild(); childElement != nil; childElement = childElement.RightSibling() { + layoutConfig := mat.ElementLayoutData(childElement) + + childSize := childElement.IdealSize() + if layoutConfig.Width.Specified { + childSize.Width = layoutConfig.Width.Value + } + childSize.Width = maxInt(childSize.Width, contentBounds.Width) + if layoutConfig.Height.Specified { + childSize.Height = layoutConfig.Height.Value + } + + maxChildSize = ui.Size{ + Width: maxInt(maxChildSize.Width, childSize.Width), + Height: maxInt(maxChildSize.Height, childSize.Height), + } + + childElement.SetBounds(ui.Bounds{ + Position: ui.NewPosition(0, -int(e.offsetY)), + Size: childSize, + }) + } + + e.maxOffsetY = float64(maxInt(0, maxChildSize.Height-contentBounds.Height)) + + element.SetIdealSize(ui.Size{ + Width: maxChildSize.Width + element.Padding().Horizontal(), + Height: maxChildSize.Height + element.Padding().Vertical(), + }) +} + +func (e *scrollPaneEssence) OnRender(element *ui.Element, canvas *ui.Canvas) { + currentTime := time.Now() + elapsedSeconds := currentTime.Sub(e.lastTick).Seconds() + e.lastTick = currentTime + + wasEnded := e.offsetY > e.maxOffsetY + e.offsetY += e.velocity * elapsedSeconds + isEnded := e.offsetY > e.maxOffsetY + if isEnded && !wasEnded { + e.onFinished() + } + + // Relayout and redraw + e.Apply(element) + element.Invalidate() +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/internal/ui/widget/home_button.go b/internal/ui/widget/button.go similarity index 78% rename from internal/ui/widget/home_button.go rename to internal/ui/widget/button.go index dde3fc2..ef71e93 100644 --- a/internal/ui/widget/home_button.go +++ b/internal/ui/widget/button.go @@ -1,33 +1,33 @@ package widget import ( + "github.com/mokiat/gog/opt" "github.com/mokiat/gomath/sprec" "github.com/mokiat/lacking/ui" co "github.com/mokiat/lacking/ui/component" "github.com/mokiat/lacking/ui/mat" - "github.com/mokiat/lacking/util/optional" ) -type HomeButtonData struct { +type ButtonData struct { Text string } -var defaultHomeButtonData = HomeButtonData{ +var defaultButtonData = ButtonData{ Text: "", } -type HomeButtonCallbackData struct { +type ButtonCallbackData struct { ClickListener mat.ClickListener } -var defaultHomeButtonCallbackData = HomeButtonCallbackData{ +var defaultButtonCallbackData = ButtonCallbackData{ ClickListener: func() {}, } -var HomeButton = co.Define(func(props co.Properties, scope co.Scope) co.Instance { +var Button = co.Define(func(props co.Properties, scope co.Scope) co.Instance { var ( - data = co.GetOptionalData(props, defaultHomeButtonData) - callbackData = co.GetOptionalCallbackData(props, defaultHomeButtonCallbackData) + data = co.GetOptionalData(props, defaultButtonData) + callbackData = co.GetOptionalCallbackData(props, defaultButtonCallbackData) ) essence := co.UseState(func() *homeButtonEssence { @@ -54,7 +54,7 @@ var HomeButton = co.Define(func(props co.Properties, scope co.Scope) co.Instance co.WithData(mat.ElementData{ Essence: essence, Padding: padding, - IdealSize: optional.Value( + IdealSize: opt.V( ui.NewSize(int(txtSize.X), int(txtSize.Y)).Grow(padding.Size()), ), }) diff --git a/internal/ui/widget/fade_in.go b/internal/ui/widget/fade_in.go new file mode 100644 index 0000000..06bc5c8 --- /dev/null +++ b/internal/ui/widget/fade_in.go @@ -0,0 +1,92 @@ +package widget + +import ( + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +type FadeInData struct { + Duration time.Duration +} + +var defaultFadeInData = FadeInData{ + Duration: time.Second, +} + +type FadeInCallbackData struct { + OnFinished func() +} + +var defaultFadeInCallbackData = FadeInCallbackData{ + OnFinished: func() {}, +} + +var FadeIn = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + data = co.GetOptionalData(props, defaultFadeInData) + callbackData = co.GetOptionalCallbackData(props, defaultFadeInCallbackData) + ) + + essence := co.UseState(func() *fadeInEssence { + return &fadeInEssence{ + lastTick: time.Now(), + } + }).Get() + essence.duration = data.Duration.Seconds() + essence.onFinished = callbackData.OnFinished + + return co.New(mat.Element, func() { + co.WithData(co.ElementData{ + Essence: essence, + Focusable: opt.V(false), + }) + co.WithLayoutData(props.LayoutData()) + co.WithChildren(props.Children()) + }) +}) + +var _ ui.ElementRenderHandler = (*fadeInEssence)(nil) + +type fadeInEssence struct { + opacity float64 + duration float64 + lastTick time.Time + onFinished func() +} + +func (e *fadeInEssence) OnRender(element *ui.Element, canvas *ui.Canvas) { + currentTime := time.Now() + elapsedSeconds := currentTime.Sub(e.lastTick).Seconds() + e.lastTick = currentTime + + wasRunning := e.opacity < 1.0 + e.opacity += elapsedSeconds / e.duration + isRunning := e.opacity < 1.0 + e.opacity = dprec.Clamp(e.opacity, 0.0, 1.0) + + if wasRunning && !isRunning { + e.onFinished() + } + + bounds := element.Bounds() + + canvas.Reset() + canvas.Rectangle( + sprec.NewVec2(0, 0), + sprec.NewVec2(float32(bounds.Width), float32(bounds.Height)), + ) + canvas.Fill(ui.Fill{ + Color: ui.RGBA(0, 0, 0, 255-uint8(e.opacity*255)), + }) + + // Force redraw. + if e.opacity < 1.0 { + element.Invalidate() + } +} diff --git a/internal/ui/widget/fade_out.go b/internal/ui/widget/fade_out.go new file mode 100644 index 0000000..6e4ae97 --- /dev/null +++ b/internal/ui/widget/fade_out.go @@ -0,0 +1,92 @@ +package widget + +import ( + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +type FadeOutData struct { + Duration time.Duration +} + +var defaultFadeOutData = FadeOutData{ + Duration: time.Second, +} + +type FadeOutCallbackData struct { + OnFinished func() +} + +var defaultFadeOutCallbackData = FadeOutCallbackData{ + OnFinished: func() {}, +} + +var FadeOut = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + data = co.GetOptionalData(props, defaultFadeOutData) + callbackData = co.GetOptionalCallbackData(props, defaultFadeOutCallbackData) + ) + + essence := co.UseState(func() *fadeOutEssence { + return &fadeOutEssence{ + lastTick: time.Now(), + } + }).Get() + essence.duration = data.Duration.Seconds() + essence.onFinished = callbackData.OnFinished + + return co.New(mat.Element, func() { + co.WithData(co.ElementData{ + Essence: essence, + Focusable: opt.V(false), + }) + co.WithLayoutData(props.LayoutData()) + co.WithChildren(props.Children()) + }) +}) + +var _ ui.ElementRenderHandler = (*fadeOutEssence)(nil) + +type fadeOutEssence struct { + opacity float64 + duration float64 + lastTick time.Time + onFinished func() +} + +func (e *fadeOutEssence) OnRender(element *ui.Element, canvas *ui.Canvas) { + currentTime := time.Now() + elapsedSeconds := currentTime.Sub(e.lastTick).Seconds() + e.lastTick = currentTime + + wasRunning := e.opacity < 1.0 + e.opacity += elapsedSeconds / e.duration + isRunning := e.opacity < 1.0 + e.opacity = dprec.Clamp(e.opacity, 0.0, 1.0) + + if wasRunning && !isRunning { + e.onFinished() + } + + bounds := element.Bounds() + + canvas.Reset() + canvas.Rectangle( + sprec.NewVec2(0, 0), + sprec.NewVec2(float32(bounds.Width), float32(bounds.Height)), + ) + canvas.Fill(ui.Fill{ + Color: ui.RGBA(0, 0, 0, uint8(e.opacity*255)), + }) + + // Force redraw. + if e.opacity < 1.0 { + element.Invalidate() + } +} diff --git a/internal/ui/widget/gearshifter.go b/internal/ui/widget/gearshifter.go new file mode 100644 index 0000000..9dcd28c --- /dev/null +++ b/internal/ui/widget/gearshifter.go @@ -0,0 +1,71 @@ +package widget + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +type GearShifterSource interface { + IsDrive() bool +} + +type GearShifterData struct { + Source GearShifterSource +} + +var GearShifter = co.DefineType(&gearShifterPresenter{}) + +var _ ui.ElementRenderHandler = (*gearShifterPresenter)(nil) + +type gearShifterPresenter struct { + Scope co.Scope `co:"scope"` + Data GearShifterData `co:"data"` + LayoutData mat.LayoutData `co:"layout"` + + driveImage *ui.Image + reverseImage *ui.Image + source GearShifterSource +} + +func (p *gearShifterPresenter) OnCreate() { + p.source = p.Data.Source + p.driveImage = co.OpenImage(p.Scope, "ui/images/drive.png") + p.reverseImage = co.OpenImage(p.Scope, "ui/images/reverse.png") +} + +func (p *gearShifterPresenter) Render() co.Instance { + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: p, + IdealSize: opt.V(ui.NewSize(200, 150)), + }) + co.WithLayoutData(p.LayoutData) + }) +} + +func (p *gearShifterPresenter) OnRender(element *ui.Element, canvas *ui.Canvas) { + bounds := element.Bounds() + area := sprec.Vec2{ + X: float32(bounds.Width), + Y: float32(bounds.Height), + } + + image := p.reverseImage + if p.source.IsDrive() { + image = p.driveImage + } + + canvas.Reset() + canvas.Rectangle(sprec.ZeroVec2(), area) + canvas.Fill(ui.Fill{ + Rule: ui.FillRuleSimple, + Color: ui.White(), + Image: image, + ImageOffset: sprec.ZeroVec2(), + ImageSize: area, + }) + element.Invalidate() // force redraw +} diff --git a/internal/ui/widget/loading.go b/internal/ui/widget/loading.go new file mode 100644 index 0000000..cda4cde --- /dev/null +++ b/internal/ui/widget/loading.go @@ -0,0 +1,120 @@ +package widget + +import ( + "math" + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +var Loading = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + essence := co.UseState(func() *loadingEssence { + return &loadingEssence{ + lastTick: time.Now(), + } + }).Get() + + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: essence, + IdealSize: opt.V(ui.NewSize(300, 300)), + }) + co.WithLayoutData(props.LayoutData()) + co.WithChildren(props.Children()) + }) +}) + +var _ ui.ElementRenderHandler = (*loadingEssence)(nil) + +type loadingEssence struct { + lastTick time.Time + greenAngle sprec.Angle + redAngle sprec.Angle +} + +func (e *loadingEssence) OnRender(element *ui.Element, canvas *ui.Canvas) { + const ( + radius = float32(140.0) + redAngleSpeed = float32(210.0) + greenAngleSpeed = float32(90.0) + anglePrecision = 2 + ) + + currentTime := time.Now() + elapsedSeconds := float32(currentTime.Sub(e.lastTick).Seconds()) + e.redAngle -= sprec.Degrees(elapsedSeconds * redAngleSpeed) + e.greenAngle += sprec.Degrees(elapsedSeconds * greenAngleSpeed) + e.lastTick = currentTime + + canvas.Push() + bounds := element.ContentBounds() + canvas.Translate(sprec.Vec2{ + X: float32(bounds.Width) / 2.0, + Y: float32(bounds.Height) / 2.0, + }) + + canvas.Reset() + for angle := sprec.Degrees(0); angle < sprec.Degrees(360); angle += sprec.Degrees(anglePrecision) { + distanceToRed := e.angleDistance(e.redAngle, angle).Degrees() + distanceToGreen := e.angleDistance(e.greenAngle, angle).Degrees() + canvas.SetStrokeSize(e.sizeFromDistances(distanceToRed, distanceToGreen)) + canvas.SetStrokeColor(e.colorFromDistances(distanceToRed, distanceToGreen)) + position := sprec.NewVec2( + sprec.Cos(angle)*radius, + -sprec.Sin(angle)*radius, + ) + if angle == 0 { + canvas.MoveTo(position) + } else { + canvas.LineTo(position) + } + } + canvas.CloseLoop() + canvas.Stroke() + + canvas.Pop() + + element.Invalidate() // force redraw +} + +func (e *loadingEssence) angleMod360(angle sprec.Angle) sprec.Angle { + degrees := float64(angle.Degrees()) + degrees = math.Mod(degrees, 360.0) + if degrees < 0.0 { + degrees += 360.0 + } + return sprec.Degrees(float32(degrees)) +} + +func (e *loadingEssence) angleDistance(a, b sprec.Angle) sprec.Angle { + modA := e.angleMod360(a) + modB := e.angleMod360(b) + rotation360 := sprec.Degrees(360.0) + if modA > modB { + forwardDelta := sprec.Abs(modA - modB) + reverseDelta := sprec.Abs(modA - modB - rotation360) + return sprec.Min(forwardDelta, reverseDelta) + } else { + forwardDelta := sprec.Abs(modB - modA) + reverseDelta := sprec.Abs(modB - modA - rotation360) + return sprec.Min(forwardDelta, reverseDelta) + } +} + +func (*loadingEssence) colorFromDistances(redDistance, greenDistance float32) ui.Color { + return ui.RGB( + uint8(2550.0/(redDistance+10.0)), + uint8(2550.0/(greenDistance+10.0)), + 0xFF, + ) +} + +func (*loadingEssence) sizeFromDistances(redDistance, greenDistance float32) float32 { + redSize := 100.0 / (redDistance/4.0 + 5.0) + greenSize := 100.0 / (greenDistance/4.0 + 5.0) + return 3.0 + redSize + greenSize +} diff --git a/internal/ui/widget/regions.go b/internal/ui/widget/regions.go new file mode 100644 index 0000000..4c7bc48 --- /dev/null +++ b/internal/ui/widget/regions.go @@ -0,0 +1,257 @@ +package widget + +import ( + "fmt" + "strings" + "time" + + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" + "github.com/mokiat/lacking/util/metrics" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +const ( + regionHeight = 45 +) + +type RegionBlockData struct { + Regions []metrics.RegionStat +} + +var RegionBlock = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + data = co.GetData[RegionBlockData](props) + ) + + var maxDepth int + for _, region := range data.Regions { + if region.Depth > maxDepth { + maxDepth = region.Depth + } + } + + essence := co.UseState(func() *regionBlockEssence { + return ®ionBlockEssence{ + selectedNodeID: metrics.NilParentID, + placement: make(map[int]regionPlacement), + graph: make(map[int]regionNode), + } + }).Get() + essence.font = co.OpenFont(scope, "mat:///roboto-bold.ttf") + essence.rebuildGraph(data.Regions) + + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: essence, + IdealSize: opt.V(ui.Size{ + Width: 200, + Height: maxDepth * regionHeight, + }), + }) + co.WithLayoutData(props.LayoutData()) + }) +}) + +var _ ui.ElementMouseHandler = (*regionBlockEssence)(nil) +var _ ui.ElementRenderHandler = (*regionBlockEssence)(nil) + +type regionBlockEssence struct { + font *ui.Font + + selectedNodeID int + graph map[int]regionNode + placement map[int]regionPlacement +} + +func (b *regionBlockEssence) rebuildGraph(stats []metrics.RegionStat) { + slices.SortFunc(stats, func(a, b metrics.RegionStat) bool { + if a.Depth == b.Depth { + return a.ID > b.ID // inversed on purpose + } + return a.Depth < b.Depth + }) + + rootDuration := time.Duration(0) + for _, stat := range stats { + if stat.ParentID == metrics.NilParentID { + rootDuration += stat.Duration + } + } + + rootSamples := 0 + for _, stat := range stats { + if stat.ParentID == metrics.NilParentID { + if stat.Samples > rootSamples { + rootSamples = stat.Samples + } + } + } + + maps.Clear(b.graph) + if rootSamples == 0 { + return + } + b.graph[metrics.NilParentID] = regionNode{ + ParentID: metrics.NilParentID, + ID: metrics.NilParentID, + FirstChild: metrics.NilParentID, + Duration: rootDuration / time.Duration(rootSamples), + DurationRatio: 1.0, + } + for _, stat := range stats { + parentNode := b.graph[stat.ParentID] + node := regionNode{ + ParentID: stat.ParentID, + ID: stat.ID, + NextSibling: parentNode.FirstChild, + FirstChild: metrics.NilParentID, + Name: stripRegionNamespace(stat.Name), + Duration: stat.Duration / time.Duration(rootSamples), + DurationRatio: float32(stat.Duration.Seconds() / rootDuration.Seconds()), + } + parentNode.FirstChild = stat.ID + b.graph[stat.ParentID] = parentNode + b.graph[stat.ID] = node + } +} + +func (b *regionBlockEssence) OnMouseEvent(element *ui.Element, event ui.MouseEvent) bool { + if event.Type == ui.MouseEventTypeDown { + if event.Button == ui.MouseButtonLeft { + position := event.Position.Translate(element.ContentBounds().Position.Inverse()) + for id, placement := range b.placement { + bounds := ui.NewBounds( + int(placement.Left), + int(placement.Top), + int(placement.Width), + int(placement.Height), + ) + if bounds.Contains(position) { + b.selectedNodeID = id + return true + } + } + } + if event.Button == ui.MouseButtonRight { + node := b.graph[b.selectedNodeID] + b.selectedNodeID = node.ParentID + return true + } + } + return false +} + +func (b *regionBlockEssence) OnRender(element *ui.Element, canvas *ui.Canvas) { + node := b.graph[b.selectedNodeID] + maps.Clear(b.placement) + b.renderRegionNode(element, canvas, node) +} + +func (b *regionBlockEssence) renderRegionNode(element *ui.Element, canvas *ui.Canvas, node regionNode) { + if node.Duration == 0 { + return + } + + parentDuration := b.graph[node.ParentID].Duration + parentPlacement, ok := b.placement[node.ParentID] + if !ok { + contentArea := element.ContentBounds() + parentPlacement = regionPlacement{ + Top: 0.0, + Left: 0.0, + Height: 0.0, + Width: float32(contentArea.Width), + FreeLeft: 0.0, + } + parentDuration = node.Duration + } + + if node.ID != metrics.NilParentID { + regionPosition := sprec.Vec2{ + X: parentPlacement.FreeLeft, + Y: parentPlacement.Top + parentPlacement.Height, + } + regionSize := sprec.Vec2{ + X: parentPlacement.Width * float32(node.Duration.Seconds()/parentDuration.Seconds()), + Y: regionHeight, + } + parentPlacement.FreeLeft += regionSize.X + b.placement[node.ParentID] = parentPlacement + + b.placement[node.ID] = regionPlacement{ + Top: regionPosition.Y, + Left: regionPosition.X, + Height: regionSize.Y, + Width: regionSize.X, + FreeLeft: regionPosition.X, + } + + canvas.SetClipRect( + regionPosition.X, + regionPosition.X+regionSize.X, + regionPosition.Y, + regionPosition.Y+regionSize.Y, + ) + canvas.Reset() + canvas.SetStrokeColor(ui.Gray()) + canvas.SetStrokeSizeSeparate(1.0, 0.0) + canvas.Rectangle(regionPosition, regionSize) + canvas.Fill(ui.Fill{ + Color: ui.ColorWithAlpha(ui.Navy(), 196), + }) + canvas.Stroke() + + name := node.Name + duration := node.Duration.Truncate(10 * time.Microsecond) + percentage := node.DurationRatio * 100 + text := fmt.Sprintf("%s\n%s | %.2f%%", name, duration, percentage) + fontSize := float32(20) + textSize := b.font.TextSize(text, fontSize) + textPosition := sprec.Vec2{ + X: regionPosition.X + (regionSize.X-textSize.X)/2.0, + Y: regionPosition.Y + (regionSize.Y-textSize.Y)/2.0, + } + canvas.FillText(text, textPosition, ui.Typography{ + Font: b.font, + Size: fontSize, + Color: ui.White(), + }) + } + + childID := node.FirstChild + for childID != metrics.NilParentID { + childNode := b.graph[childID] + b.renderRegionNode(element, canvas, childNode) + childID = childNode.NextSibling + } +} + +func stripRegionNamespace(name string) string { + if index := strings.IndexRune(name, ':'); index >= 0 { + return name[index+1:] + } + return name +} + +type regionNode struct { + ParentID int + ID int + NextSibling int + FirstChild int + Name string + Duration time.Duration + DurationRatio float32 +} + +type regionPlacement struct { + Top float32 + Left float32 + Height float32 + Width float32 + FreeLeft float32 +} diff --git a/internal/ui/widget/separator.go b/internal/ui/widget/separator.go new file mode 100644 index 0000000..3c50a83 --- /dev/null +++ b/internal/ui/widget/separator.go @@ -0,0 +1,40 @@ +package widget + +import ( + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +var Separator = co.DefineType(&separatorPresenter{}) + +type separatorPresenter struct { + LayoutData mat.LayoutData `co:"layout"` +} + +var _ ui.ElementRenderHandler = (*separatorPresenter)(nil) + +func (p *separatorPresenter) Render() co.Instance { + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: p, + }) + co.WithLayoutData(p.LayoutData) + }) +} + +func (p *separatorPresenter) OnRender(element *ui.Element, canvas *ui.Canvas) { + bounds := element.Bounds() // take padding into consideration + area := sprec.Vec2{ + X: float32(bounds.Width), + Y: float32(bounds.Height), + } + + canvas.Reset() + canvas.Rectangle(sprec.ZeroVec2(), area) + canvas.Fill(ui.Fill{ + Rule: ui.FillRuleSimple, + Color: ui.Black(), + }) +} diff --git a/internal/ui/widget/speedometer.go b/internal/ui/widget/speedometer.go new file mode 100644 index 0000000..153c410 --- /dev/null +++ b/internal/ui/widget/speedometer.go @@ -0,0 +1,93 @@ +package widget + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +type SpeedometerSource interface { + Velocity() float64 +} + +type SpeedometerData struct { + Source SpeedometerSource +} + +var Speedometer = co.Define(func(props co.Properties, scope co.Scope) co.Instance { + var ( + data = co.GetData[SpeedometerData](props) + ) + + essence := co.UseState(func() *speedometerEssence { + return &speedometerEssence{ + speedometerImage: co.OpenImage(scope, "ui/images/speedometer.png"), + needleImage: co.OpenImage(scope, "ui/images/needle.png"), + source: data.Source, + } + }).Get() + + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: essence, + IdealSize: opt.V(ui.NewSize(300, 150)), + }) + co.WithLayoutData(props.LayoutData()) + co.WithChildren(props.Children()) + }) +}) + +var _ ui.ElementRenderHandler = (*speedometerEssence)(nil) + +type speedometerEssence struct { + speedometerImage *ui.Image + needleImage *ui.Image + source SpeedometerSource +} + +func (e *speedometerEssence) OnRender(element *ui.Element, canvas *ui.Canvas) { + const ( + maxVelocity = 200.0 // TODO: Configurable + ) + + bounds := element.Bounds() + area := sprec.Vec2{ + X: float32(bounds.Width), + Y: float32(bounds.Height), + } + + canvas.Push() + canvas.Reset() + canvas.Rectangle(sprec.ZeroVec2(), area) + canvas.Fill(ui.Fill{ + Rule: ui.FillRuleSimple, + Color: ui.White(), + Image: e.speedometerImage, + ImageOffset: sprec.ZeroVec2(), + ImageSize: area, + }) + + needleSize := sprec.NewVec2(34.0, 150.0) + canvas.Translate(sprec.NewVec2( + area.X/2.0, + area.Y-20, + )) + velocity := e.source.Velocity() * 3.6 // from m/s to km/h + + canvas.Rotate(sprec.Degrees(-90 + 180.0*(float32(velocity/maxVelocity)))) + canvas.Reset() + canvas.Rectangle(sprec.NewVec2(-needleSize.X/2.0, 20-needleSize.Y), needleSize) + canvas.Fill(ui.Fill{ + Rule: ui.FillRuleSimple, + Color: ui.White(), + Image: e.needleImage, + ImageOffset: sprec.NewVec2(-needleSize.X/2.0, 20-needleSize.Y), + ImageSize: needleSize, + }) + + canvas.Pop() + + element.Invalidate() // force redraw +} diff --git a/internal/ui/widget/toggle.go b/internal/ui/widget/toggle.go new file mode 100644 index 0000000..b746550 --- /dev/null +++ b/internal/ui/widget/toggle.go @@ -0,0 +1,106 @@ +package widget + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/ui" + co "github.com/mokiat/lacking/ui/component" + "github.com/mokiat/lacking/ui/mat" +) + +const ( + toggleFontSize = float32(24.0) +) + +type ToggleData struct { + Text string + Selected bool +} + +type ToggleCallbackData struct { + ClickListener mat.ClickListener +} + +var Toggle = co.DefineType(&togglePresenter{}) + +var _ ui.ElementRenderHandler = (*togglePresenter)(nil) + +type togglePresenter struct { + *mat.ButtonBaseEssence + + Scope co.Scope `co:"scope"` + Data ToggleData `co:"data"` + CallbackData ToggleCallbackData `co:"callback"` + LayoutData mat.LayoutData `co:"layout"` + + font *ui.Font +} + +func (p *togglePresenter) OnCreate() { + if p.CallbackData.ClickListener == nil { + p.CallbackData.ClickListener = func() {} + } + p.ButtonBaseEssence = mat.NewButtonBaseEssence(p.CallbackData.ClickListener) + + p.font = co.OpenFont(p.Scope, "mat:///roboto-bold.ttf") +} + +func (p *togglePresenter) Render() co.Instance { + padding := ui.Spacing{ + Left: 5, + Right: 5, + Top: 2, + Bottom: 2, + } + + txtSize := p.font.TextSize(p.Data.Text, toggleFontSize) + + return co.New(mat.Element, func() { + co.WithData(mat.ElementData{ + Essence: p, + Padding: padding, + IdealSize: opt.V( + ui.NewSize(int(txtSize.X)+int(txtSize.Y)+5, int(txtSize.Y)).Grow(padding.Size()), + ), + }) + co.WithLayoutData(p.LayoutData) + }) +} + +func (p *togglePresenter) OnRender(element *ui.Element, canvas *ui.Canvas) { + var fontColor ui.Color + switch p.State() { + case mat.ButtonStateOver: + fontColor = ui.RGB(0x00, 0xB2, 0x08) + case mat.ButtonStateDown: + fontColor = ui.RGB(0x00, 0x33, 0x00) + default: + fontColor = ui.White() + } + if p.Data.Selected { + fontColor = ui.RGB(0x00, 0xB2, 0x08) + } + + bounds := element.ContentBounds() // take padding into consideration + area := sprec.Vec2{ + X: float32(bounds.Width), + Y: float32(bounds.Height), + } + + canvas.Reset() + canvas.Circle(sprec.NewVec2(area.Y/2.0, area.Y/2.0), area.Y/2.0) + canvas.Fill(ui.Fill{ + Rule: ui.FillRuleSimple, + Color: fontColor, + }) + + canvas.Reset() + canvas.FillText(p.Data.Text, sprec.NewVec2( + float32(area.Y+10), + float32(0.0), + ), ui.Typography{ + Font: p.font, + Size: toggleFontSize, + Color: fontColor, + }) +} diff --git a/preview.png b/preview.png index 735a9b7..a05d32d 100644 Binary files a/preview.png and b/preview.png differ diff --git a/resources/icon.png b/resources/icon.png deleted file mode 100644 index 9d8872b..0000000 --- a/resources/icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4c4c3f17f43f872614271f0f9f0aed2a5fc536ac7519173d33e4fe656ca94fe -size 2408 diff --git a/resources/images/barrier.png b/resources/images/barrier.png deleted file mode 100644 index 6c12420..0000000 --- a/resources/images/barrier.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:29ec8fada62dd08243c0505646c4003578dd8cc88d033f2044551e00f0e3f348 -size 12680 diff --git a/resources/images/body.png b/resources/images/body.png deleted file mode 100644 index 9fb0927..0000000 --- a/resources/images/body.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:787c19b2ad0b63671ec641d2157e420088288222f9e43a9c5560e6c0e2e067d4 -size 356456 diff --git a/resources/images/concrete.png b/resources/images/concrete.png deleted file mode 100644 index ab286bf..0000000 --- a/resources/images/concrete.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1bc999a4273c5e794d9817253c652acc214d3e6264cbdd5e81bf5763f4aa3839 -size 87 diff --git a/resources/images/day.hdr b/resources/images/day.hdr new file mode 100644 index 0000000..b27f150 --- /dev/null +++ b/resources/images/day.hdr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0dde3f2cbd5b6264b1e34b2fe03160375be90051dce1013dc8c377e9fffa609 +size 8388673 diff --git a/resources/images/grass.png b/resources/images/grass.png deleted file mode 100644 index 4a7500a..0000000 --- a/resources/images/grass.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1341cd1559516d102fe8793b41340efe532910af173d8095107503c37361e5b4 -size 33796 diff --git a/resources/images/gravel.png b/resources/images/gravel.png deleted file mode 100644 index 8f7dc15..0000000 --- a/resources/images/gravel.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5acbb3af93032713deffac05592af06d425334112300ffd619abc8ebdd10a9ce -size 139719 diff --git a/resources/images/leafy_tree.png b/resources/images/leafy_tree.png deleted file mode 100644 index 97b0108..0000000 --- a/resources/images/leafy_tree.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f45660a15a7dd7c2ceca3e6f9ae7316a287f79a0b2f7cd78297dee94568ebf50 -size 243533 diff --git a/resources/images/night.exr b/resources/images/night.exr new file mode 100644 index 0000000..f2b2f96 --- /dev/null +++ b/resources/images/night.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27596a5c52159b7623b9c3cb7349038c67944a2579457de0357a1c2abc835812 +size 2055973 diff --git a/resources/images/road.png b/resources/images/road.png deleted file mode 100644 index 8298dce..0000000 --- a/resources/images/road.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae18d2bcdd92fc1e13a56704a74152ea9c0a268d6c0fdab58d6a06dbd49ebbca -size 183 diff --git a/resources/images/rusty_metal_02_diff_512.png b/resources/images/rusty_metal_02_diff_512.png deleted file mode 100644 index 43d5021..0000000 --- a/resources/images/rusty_metal_02_diff_512.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6fbd1a250979a415fe756958dc2549296d3f487be55c11f04b8ecc4a024db764 -size 591288 diff --git a/resources/images/syferfontein.hdr b/resources/images/syferfontein.hdr deleted file mode 100644 index c32817c..0000000 --- a/resources/images/syferfontein.hdr +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfe7de9fec8d026e53bd1c966670eac570a322ea79587199e7ca29ec8e49a7ef -size 91708583 diff --git a/resources/images/wheel.png b/resources/images/wheel.png deleted file mode 100644 index 28c07e5..0000000 --- a/resources/images/wheel.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d0b33ca04f0bec9e5a7f603dc22dab02755450f3601359c5abbbe5f0acef452 -size 85522 diff --git a/resources/levels/forest-day.json b/resources/levels/forest-day.json new file mode 100644 index 0000000..98dede4 --- /dev/null +++ b/resources/levels/forest-day.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb0d35e0150c8cbd3afaaff2d92b035f38c8a956bcddf5bcc289c13c1b1b2955 +size 416 diff --git a/resources/levels/forest-night.json b/resources/levels/forest-night.json new file mode 100644 index 0000000..43716ba --- /dev/null +++ b/resources/levels/forest-night.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3a615551f16de45453e9e1bc175cf9335f967f1c89d6863171ab1a8fb39d996 +size 416 diff --git a/resources/levels/forest.json b/resources/levels/forest.json deleted file mode 100644 index 7f9e83c..0000000 --- a/resources/levels/forest.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ebb5072497624079efda19573f9ce06f5d228df19e9cb6fb8ac5665aa2f1929 -size 387692 diff --git a/resources/levels/highway.json b/resources/levels/highway.json deleted file mode 100644 index e9333d6..0000000 --- a/resources/levels/highway.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62b0833feee714dd17ff19fb4e0b08be3769d548dfdcfdfebf7bb80511620e1b -size 225868 diff --git a/resources/levels/home-screen.json b/resources/levels/home-screen.json new file mode 100644 index 0000000..6a1d380 --- /dev/null +++ b/resources/levels/home-screen.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dab9272416ac0d41a3be61ef501327e2236d3b31cccacb42a12329d8098582eb +size 237 diff --git a/resources/levels/playground.json b/resources/levels/playground.json deleted file mode 100644 index c12e590..0000000 --- a/resources/levels/playground.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a285511acfe39a9c5b140b049c2ebea860b0af929e3e5df7682b5273a74a580 -size 1897 diff --git a/resources/licenses.tmpl b/resources/licenses.tmpl new file mode 100644 index 0000000..a70de0b --- /dev/null +++ b/resources/licenses.tmpl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcfb22444f0232b51649b5965921f4e208ef1d6d2f9530d93573465063ad66af +size 157 diff --git a/resources/licenses.txt b/resources/licenses.txt new file mode 100644 index 0000000..e125054 --- /dev/null +++ b/resources/licenses.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01b7ec3aff8cb69ec1e02244713d6f34e5ba2e700af76ce71c4365ccac35b39c +size 45111 diff --git a/resources/models/forest.glb b/resources/models/forest.glb new file mode 100644 index 0000000..8bc1e00 --- /dev/null +++ b/resources/models/forest.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a6500d7d5ef65d2974e6ba822ba2932d9c0e427ae9c2cf2e875cb668e197fea +size 59174024 diff --git a/resources/models/home-screen.glb b/resources/models/home-screen.glb new file mode 100644 index 0000000..b67aced --- /dev/null +++ b/resources/models/home-screen.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1642115b5a32aaa31114c08e97791b05ec8a939a2a2ee1046cd7e453051dc9a +size 3013632 diff --git a/resources/models/leafy_tree.bin b/resources/models/leafy_tree.bin deleted file mode 100644 index 8805a16..0000000 --- a/resources/models/leafy_tree.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1efc907eb9595bf0f8fcad922f45491b180b9af812add1e799f761bc75966359 -size 26116 diff --git a/resources/models/leafy_tree.gltf b/resources/models/leafy_tree.gltf deleted file mode 100644 index 0f49f34..0000000 --- a/resources/models/leafy_tree.gltf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:762ce4b616d57dc020b731da15178e732a301edf2d07a68e7724d48a214aa213 -size 3021 diff --git a/resources/models/street_lamp.bin b/resources/models/street_lamp.bin deleted file mode 100644 index 4686764..0000000 --- a/resources/models/street_lamp.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea632ad0d198e0bfcd5b0ad8c781eb85d48d0214468877cb550e2544ba518bc4 -size 2436 diff --git a/resources/models/street_lamp.gltf b/resources/models/street_lamp.gltf deleted file mode 100644 index 5d31b83..0000000 --- a/resources/models/street_lamp.gltf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:46de0cc33028a7e1a5003db35935ddcc6ef7c3ea1916ab7eaac704ec55524361 -size 5042 diff --git a/resources/models/suv.bin b/resources/models/suv.bin deleted file mode 100644 index 0287397..0000000 --- a/resources/models/suv.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58fa892bd2dedc469aa9f4aed38c60de31f2cb0742c43c3991ea5107edf66f62 -size 229288 diff --git a/resources/models/suv.glb b/resources/models/suv.glb new file mode 100644 index 0000000..c56bbf2 --- /dev/null +++ b/resources/models/suv.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c1eae801cd1d76f8488b2efabf3f7a23aa10682a1221cf113d531d588de73ac +size 1342832 diff --git a/resources/models/suv.gltf b/resources/models/suv.gltf deleted file mode 100644 index c7aa8c3..0000000 --- a/resources/models/suv.gltf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1a283b59b75ead5d7852a0b2970b0f69b2f592b39b96b227ae60f5da110a7f08 -size 19730 diff --git a/resources/resources.go b/resources/resources.go index 0f3f4a3..e017c9f 100644 --- a/resources/resources.go +++ b/resources/resources.go @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0abb7122152a9ae55b5e4c23effdbc607b38e31494ce8d99d460ba346a7a03d -size 65 +oid sha256:d73a8ad77e79ec4f9ee2416abf45b75210b892022bf63ba7fb85dd0427d5dc60 +size 110 diff --git a/resources/ui/images/background.png b/resources/ui/images/background.png deleted file mode 100644 index 88bf738..0000000 --- a/resources/ui/images/background.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af9cc71e54b23a54719575488bbbc15bb4d52ad202ac47321f9ca2dad8b66c2c -size 293519 diff --git a/resources/ui/images/drive.png b/resources/ui/images/drive.png new file mode 100644 index 0000000..a4943d0 --- /dev/null +++ b/resources/ui/images/drive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fe510752139ad0fb95a2cf1596ded5cf3bb3c61e1698999e506d32f6d303d73 +size 13298 diff --git a/resources/ui/images/gamepad.png b/resources/ui/images/gamepad.png new file mode 100644 index 0000000..a4b6ea6 --- /dev/null +++ b/resources/ui/images/gamepad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:035186cee587ffc0ec1bc708f95feca07dabe24397e273d253ea51198dfe7f5d +size 43872 diff --git a/resources/ui/images/icon.png b/resources/ui/images/icon.png new file mode 100644 index 0000000..5d57c31 --- /dev/null +++ b/resources/ui/images/icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cf03e899e80f153f3d90009c9d18ce68110f07755250726fb5f06bef5224824 +size 13088 diff --git a/resources/ui/images/keyboard.png b/resources/ui/images/keyboard.png new file mode 100644 index 0000000..a1f369d --- /dev/null +++ b/resources/ui/images/keyboard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8afe15de5d7f568b47abf37c65863ad38ba8480fdfdfa808c8a829663801c0f9 +size 37081 diff --git a/resources/ui/images/mouse.png b/resources/ui/images/mouse.png new file mode 100644 index 0000000..a6d09e5 --- /dev/null +++ b/resources/ui/images/mouse.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dbfcae19ee6b9c618b9b669d36493958e5cf826578c1b35083ef11bac8b8f0d +size 40867 diff --git a/resources/ui/images/needle.png b/resources/ui/images/needle.png new file mode 100644 index 0000000..f30419d --- /dev/null +++ b/resources/ui/images/needle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c61eead235c3efb0a0a2562d3cea1fa582315634734afd88f016587dc717a520 +size 5070 diff --git a/resources/ui/images/reverse.png b/resources/ui/images/reverse.png new file mode 100644 index 0000000..5914028 --- /dev/null +++ b/resources/ui/images/reverse.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfabf81226c72056d898bfaabf3ed0aa020a86dfa3076e9668d79c4bc4b5420d +size 13370 diff --git a/resources/ui/images/speedometer.png b/resources/ui/images/speedometer.png new file mode 100644 index 0000000..4c99f8e --- /dev/null +++ b/resources/ui/images/speedometer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fb3f2eba06e024f9e48e6afc78ff5b9ef62f6837a0e1dc4144004609451612f +size 22746 diff --git a/resources/web/index.html b/resources/web/index.html index 4d9b54c..deeccd5 100644 --- a/resources/web/index.html +++ b/resources/web/index.html @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebcf5d4ab0d7e9931acfc6889b653edd0585208d8425dc3fd3b761d59da08681 -size 1007 +oid sha256:fc4ee010574d16ebdbbd409830cc809b1a362f5f21eab2214c259119d1e80446 +size 934 diff --git a/resources/web/main.css b/resources/web/main.css index 7a6dc6c..dbf7e98 100644 --- a/resources/web/main.css +++ b/resources/web/main.css @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87de1c684cc2f9baf92b6404b37e780ca5d6853b0e94f4a815051706badc49f3 -size 537 +oid sha256:16705f0bd99924ec8c4df01e2a3e5d5d6de6cfa8bfee09eb5cf96752d0898a72 +size 403 diff --git a/resources/web/main.js b/resources/web/main.js index f21e950..ba422d6 100644 --- a/resources/web/main.js +++ b/resources/web/main.js @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36f25b00ba7e754f46020996374e4f4015ab92fb7ad4b16f3463e0e4c880a6bc -size 950 +oid sha256:6446310e439fa5ab69c9f6da4d58cfd0d7bcbb8a6e838ca13c572e3e3f8e391c +size 940