# План: M1 - Core Engine & Basic Rendering **Цель:** Создать базовое окно игры, отрисовать хоккейную площадку и одну статичную шайбу, настроить основной игровой цикл. **Архитектура:** - Использование Ebitengine для рендеринга. - Разделение на `World` (состояние) и `Renderer` (отрисовка). - Точка входа в `cmd/game/main.go`. **Стек:** Go, Ebitengine. **Спека:** [docs/specs/2026-05-08-hockey-design.md](../specs/2026-05-08-hockey-design.md) --- ## Файловая структура - `cmd/game/main.go` — точка входа, инициализация Ebitengine. - `internal/game/world.go` — структура World, хранящая состояние игры. - `internal/render/renderer.go` — логика отрисовки объектов на экране. - `internal/entities/puck.go` — определение структуры Puck. ## Задачи ### Задача 1: Инициализация проекта и окна **Файлы:** - Создать: `cmd/game/main.go` - [ ] **Шаг 1: Минимальный код для запуска окна** ```go package main import ( "log" "github.com/hajimehoshi/ebiten/v2" ) type Game struct{} func (g *Game) Update() error { return nil } func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(ebiten.ColorWhite) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return 800, 600 } func main() { ebiten.SetWindowSize(800, 600) ebiten.SetWindowTitle("Hockey Sim") if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } } ``` - [ ] **Шаг 2: Запуск и проверка** `go run cmd/game/main.go` Ожидание: Открывается белое окно 800x600. - [ ] **Шаг 3: Коммит** `git add cmd/game/main.go` `git commit -m "feat(engine): initialize ebitengine window"` ### Задача 2: Определение сущности Шайбы **Файлы:** - Создать: `internal/entities/puck.go` - [ ] **Шаг 1: Структура Puck** ```go package entities type Puck struct { X, Y float64 } func NewPuck(x, y float64) *Puck { return &Puck{X: x, Y: y} } ``` - [ ] **Шаг 2: Коммит** `git add internal/entities/puck.go` `git commit -m "feat(entities): add puck entity"` ### Задача 3: Создание игрового мира **Файлы:** - Создать: `internal/game/world.go` - [ ] **Шаг 1: Структура World** ```go package game import "football/internal/entities" type World struct { Puck *entities.Puck } func NewWorld() *World { return &World{ Puck: entities.NewPuck(400, 300), } } ``` - [ ] **Шаг 2: Коммит** `git add internal/game/world.go` `git commit -m "feat(game): add world state"` ### Задача 4: Реализация базового рендерера **Файлы:** - Создать: `internal/render/renderer.go` - [ ] **Шаг 1: Функция отрисовки мира** ```go package render import ( "football/internal/game" "github.com/hajimehoshi/ebiten/v2" "image/color" ) type Renderer struct{} func (r *Renderer) DrawWorld(screen *ebiten.Image, world *game.World) { // Отрисовка льда (белый фон) screen.Fill(color.RGBA{240, 240, 240, 255}) // Отрисовка шайбы (черный квадрат для начала) // В Ebitengine для простых фигур используем vector или маленькие изображения. // Для M1 достаточно закрасить область. } ``` *Примечание: Для полноценного круга потребуется vector package, но для M1 начнем с простого заполнения.* - [ ] **Шаг 2: Коммит** `git add internal/render/renderer.go` `git commit -m "feat(render): add basic world renderer"` ### Задача 5: Интеграция в Game Loop **Файлы:** - Изменить: `cmd/game/main.go` - [ ] **Шаг 1: Подключение World и Renderer** ```go package main import ( "football/internal/game" "football/internal/render" "github.com/hajimehoshi/ebiten/v2" "log" ) type Game struct { world *game.World renderer *render.Renderer } func (g *Game) Update() error { return nil } func (g *Game) Draw(screen *ebiten.Image) { g.renderer.DrawWorld(screen, g.world) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return 800, 600 } func main() { ebiten.SetWindowSize(800, 600) ebiten.SetWindowTitle("Hockey Sim") gameInstance := &Game{ world: game.NewWorld(), renderer: &render.Renderer{}, } if err := ebiten.RunGame(gameInstance); err != nil { log.Fatal(err) } } ``` - [ ] **Шаг 2: Запуск и проверка** `go run cmd/game/main.go` Ожидание: Окно с серым фоном. - [ ] **Шаг 3: Коммит** `git add cmd/game/main.go` `git commit -m "feat(engine): integrate world and renderer into loop"`