diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-06-06 20:59:12 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-06-06 20:59:12 +0200 |
| commit | d9a54f1319cc9dcc2861cecc9edfde14cf039c5b (patch) | |
| tree | 74df791d6eda0ffe6c09cb32e5ffd76c4849cbac /main.go | |
| parent | 18592fd9f520a33ab307e496b995c1a7bf284893 (diff) | |
Add examples catalog with subcommand dispatch
- Extract game engine from package main to engine/ library package
- Create examples/ package with registry, Entry type, and BuilderDeps
- Register default world as an example (moved from main())
- Add combat example: two teams of units facing each other
- Rewrite main.go as thin dispatch: go run . example [name]
- Update README with examples catalog one-liner
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 227 |
1 files changed, 42 insertions, 185 deletions
@@ -2,187 +2,28 @@ package main import ( "bytes" + "fmt" "image" "image/color" _ "image/jpeg" "os" "runtime/pprof" - "time" - "age_of_empires/ecs" - "age_of_empires/physics" + "age_of_empires/engine" + "age_of_empires/examples" "age_of_empires/selection" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/martinlehoux/kagamigo/kcore" "golang.org/x/exp/slog" - -) - -type Action string - -const ( - Selecting Action = "selecting" - Patrolling Action = "patrolling" -) - -var soilColor = color.RGBA{0x60, 0x40, 0x20, 0xff} - -type Game struct { - CurrentAction Action - Camera Camera - GlobalSelection selection.GlobalSelection - ResourceAmount int - FaceSource *text.GoTextFaceSource - UnitBuilder EntityBuilder - NowFunc func() time.Time - // Components - Mask []ecs.Mask - // Entities []*Entity - Position []physics.Point - RelBounds []physics.Rectangle - Image []*ebiten.Image - Selectable []selection.Selectable - Move []physics.Move - Order []OrderKind - PatrolOrder []PatrolOrder - ResourceGatherer []ResourceGatherer - ResourceSource []ResourceSource - ResourceStorage []ResourceStorage - Spawn []Spawn -} - -func (g Game) Now() time.Time { - if g.NowFunc != nil { - return g.NowFunc() - } - return time.Now() -} - -func (g *Game) Append(components Components, mask ecs.Mask) ecs.Entity { - g.Mask = append(g.Mask, mask) - g.Position = append(g.Position, components.Position) - g.RelBounds = append(g.RelBounds, components.RelBounds) - g.Image = append(g.Image, components.Image) - g.Selectable = append(g.Selectable, components.Selectable) - g.Move = append(g.Move, components.Move) - g.Order = append(g.Order, components.Order) - g.ResourceGatherer = append(g.ResourceGatherer, components.ResourceGatherer) - g.ResourceSource = append(g.ResourceSource, components.ResourceSource) - g.ResourceStorage = append(g.ResourceStorage, components.ResourceStorage) - g.Spawn = append(g.Spawn, components.Spawn) - return ecs.Entity(len(g.Mask) - 1) -} - -func (g *Game) getMoveMap() physics.MoveMap { - required := ecs.CM_Position - blocked := map[physics.Point]bool{} - for i, mask := range g.Mask { - if mask&required == required { - blocked[g.Position[i]] = true - } - } - return physics.MoveMap{Width: WorldWidth, Height: WorldHeight, Blocked: blocked} -} - -func (g *Game) entityAt(position physics.Point) ecs.Entity { - required := ecs.CM_Position - for i, mask := range g.Mask { - if mask&required == required && g.Position[i] == position { - return ecs.Entity(i) - } - } - return -1 -} - -func getAllStorageDockings(g *Game) []physics.Point { - storageDockings := []physics.Point{} - required := ecs.CM_Position | ecs.CM_ResourceStorage - for i, mask := range g.Mask { - if mask&required == required { - position := g.Position[i] - storageDockings = append(storageDockings, physics.AdjacentPoints(position)...) - } - } - return storageDockings -} - -const ( - KeySpawnRequest = ebiten.KeyS - KeyPatrolRequest = ebiten.KeyQ ) -func (g *Game) updateSelecting(input Input, moveMap physics.MoveMap) { - if input.LeftMouseDown { - g.GlobalSelection.Start(input.Cursor) - g.GlobalSelection.StartScreen(input.ScreenCursor) - } - if input.RightMouseUp { - destination := input.Cursor.Div(100).Mul(100) - slog.Info("destination", slog.String("destination", destination.String())) - for _, i := range g.GlobalSelection.Selected { - MainAction(g, i, destination, moveMap) - } - } - if input.LeftMouseUp { - if g.GlobalSelection.IsActive(input.Cursor) { - if g.GlobalSelection.IsArea(input.Cursor) { - g.GlobalSelection.SelectMultiple(input.Cursor, g.Mask, g.Selectable, g.Position, g.RelBounds) - } else { - g.GlobalSelection.SelectSingle(input.Cursor, g.Mask, g.Selectable, g.Position, g.RelBounds) - } - } - } - if input.SpawnPressed { - required := ecs.CM_Spawn - for _, i := range g.GlobalSelection.Selected { - if g.Mask[i]&required == required { - spawn := &g.Spawn[i] - if g.ResourceAmount >= spawn.UnitResourceCost { - g.ResourceAmount -= spawn.UnitResourceCost - spawn.Requests = append(spawn.Requests, SpawnRequest{Start: g.Now()}) - } - } - } - } -} - -func (g *Game) UpdateSimulation(input Input) { - moveMap := g.getMoveMap() - - if input.EscapePressed { - g.GlobalSelection.Unselect(g.Selectable) - g.CurrentAction = Selecting - } else if input.PatrolPressed { - g.CurrentAction = Patrolling +func listExamples() { + fmt.Println("Available examples:") + for name, entry := range examples.Registry { + fmt.Printf(" %-12s %s\n", name, entry.Description) } - - switch g.CurrentAction { - case Selecting: - g.updateSelecting(input, moveMap) - case Patrolling: - if input.RightMouseUp { - destination := input.Cursor.Div(100).Mul(100) - StartPatrolSystem(destination, g.Mask, g.Position, g.Move, g.Order, g.PatrolOrder) - g.CurrentAction = Selecting - } - } - - physics.UpdateMoveSystem(moveMap, g.Mask, g.Move, g.Position) - UpdateSpawnSystem(g, g.Now(), g.Mask, g.Spawn, g.Position) - UpdateGatherSystem(g, g.Now(), g.getMoveMap(), g.Mask, g.Order, g.Position, g.Move, g.ResourceGatherer) - UpdatePatrolSystem(g.getMoveMap(), g.Mask, g.Order, g.PatrolOrder, g.Position, g.Move) -} - -func (g *Game) Update() error { - g.Camera.Update(1.0 / 60.0) - g.UpdateSimulation(CollectInput(&g.Camera)) - return nil -} - -func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { - return ScreenWidth, ScreenHeight } func main() { @@ -197,7 +38,7 @@ func main() { } logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) slog.SetDefault(slog.New(logHandler)) - ebiten.SetWindowSize(ScreenWidth, ScreenHeight) + ebiten.SetWindowSize(engine.ScreenWidth, engine.ScreenHeight) ebiten.SetWindowTitle("Age of Empire") icon, err := os.Open("icon-crop.jpg") kcore.Expect(err, "failed to open icon") @@ -205,29 +46,45 @@ func main() { iconImg, _, err := image.Decode(icon) kcore.Expect(err, "failed to decode icon") ebiten.SetWindowIcon([]image.Image{iconImg}) - game := &Game{ - ResourceAmount: 150, - Camera: Camera{ - X: 1000, - Y: 1000, - Zoom: 1.0, - }, - } fontData, err := os.ReadFile("fonts/Cinzel-VariableFont_wght.ttf") kcore.Expect(err, "failed to read font") s, err := text.NewGoTextFaceSource(bytes.NewReader(fontData)) kcore.Expect(err, "failed to create font source") - game.FaceSource = s - ironMineBuilder := NewEntityBuilder().WithSolid(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x80, 0x80, 0x80, 0xff})).WithResourceSource(1000).WithSelectable("square", selection.Building) - game.Append(ironMineBuilder.WithPosition(physics.Point{X: 1000, Y: 1000}).Build()) - townCenterBuilder := NewEntityBuilder().WithSolid(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x0, 0x0, 0xff, 0xff})).WithResourceStorage().WithSelectable("square", selection.Building).WithSpawn(NewSpawn(50, 1*time.Second)) - townCenter := game.Append(townCenterBuilder.WithPosition(physics.Point{X: 1000, Y: 2000}).Build()) - game.UnitBuilder = NewEntityBuilder().WithSolid(NewFilledCircleImage(100, color.White)).WithSelectable("round", selection.Unit).WithMove().WithOrder().WithResourceGatherer(15) - for i := 0; i < 0; i++ { - spawnPosition, _ := physics.Closest(game.Position[townCenter], physics.AdjacentPoints(game.Position[townCenter]), game.getMoveMap()) - game.Append(game.UnitBuilder.WithPosition(spawnPosition).Build()) + + baseUnitBuilder := engine.NewEntityBuilder(). + WithSolid(engine.NewFilledCircleImage(100, color.White)). + WithSelectable("round", selection.Unit). + WithMove().WithOrder().WithResourceGatherer(15) + + game := &engine.Game{ + FaceSource: s, + UnitBuilder: baseUnitBuilder, } - game.CurrentAction = Selecting + + deps := examples.BuilderDeps{ + FaceSource: s, + UnitBuilder: baseUnitBuilder, + } + + if len(os.Args) >= 2 && os.Args[1] == "example" { + if len(os.Args) >= 3 { + name := os.Args[2] + entry, ok := examples.Registry[name] + if !ok { + fmt.Fprintf(os.Stderr, "Unknown example %q\n\n", name) + listExamples() + os.Exit(1) + } + slog.Info("running example", slog.String("name", name)) + entry.Build(game, deps) + } else { + listExamples() + os.Exit(0) + } + } else { + examples.Registry["default"].Build(game, deps) + } + ebiten.SetRunnableOnUnfocused(true) kcore.Expect(ebiten.RunGame(game), "failed to run game") } |
