From 1b674aa54513febeb3d317027977e0fd80c992ff Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Thu, 21 May 2026 10:24:37 +0300 Subject: [PATCH] feat(crew): restrict movement to room cell slots Add per-room cell slots and limit crew movement to unoccupied cells. - Add Cells field to Room and populate two slots per player room. - Shift enemy-ship cells by the same x-offset as rooms. - Build a cellSet lookup and gate moves on it, skipping occupied cells. - Add cellOccupied check so two crew cannot share a slot. - Render dim cell markers in render.go. - Cover cellSet and occupancy with tests; tighten hit-flash test timing. --- combat_test.go | 2 +- main.go | 23 +++++++++++++++++++++-- render.go | 7 +++++++ ship.go | 21 ++++++++++++++++----- tiles.go | 10 ++++++++++ tiles_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 8 deletions(-) diff --git a/combat_test.go b/combat_test.go index d4085c4183d660b99bcd9d6b383a255072861cc7..0d53a3f8c9d88b70218aba6cd9fac76f4696ea13 100644 --- a/combat_test.go +++ b/combat_test.go @@ -145,7 +145,7 @@ func TestUpdateCombat_hitFlashTicksDown(t *testing.T) { rng := rand.New(rand.NewSource(42)) sys := NewStartingSystems() // all power 0 — no weapons, no enemy fire crosses interval c := NewCombat() - c.Player.HitFlashT = 0.05 + c.Player.HitFlashT = 0.01 c.Enemy.HitFlashT = 0.15 updateCombat(&c, sys, 1.0/60.0, rng) if c.Player.HitFlashT != 0 { diff --git a/main.go b/main.go index e6281065be6950174533f6657adb632c35dd64c6..6981f6aaddec10eb050686e25320e0abece13c15 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( type Game struct { ship Ship walk map[[2]int]bool + cells map[[2]int]bool crew []Crew selectedCrew int lastTime time.Time @@ -75,9 +76,13 @@ func (g *Game) Update() error { } } - if g.selectedCrew >= 0 && g.walk[[2]int{tx, ty}] { + if g.selectedCrew >= 0 && g.cells[[2]int{tx, ty}] { + dest := [2]int{tx, ty} + if g.cellOccupied(dest, g.selectedCrew) { + return nil + } from := [2]int{g.crew[g.selectedCrew].TileX, g.crew[g.selectedCrew].TileY} - g.crew[g.selectedCrew].Path = bfsPath(g.walk, from, [2]int{tx, ty}) + g.crew[g.selectedCrew].Path = bfsPath(g.walk, from, dest) g.crew[g.selectedCrew].MoveT = 0 } } @@ -99,6 +104,18 @@ func (g *Game) Update() error { return nil } +func (g *Game) cellOccupied(cell [2]int, excludeCrew int) bool { + for i, c := range g.crew { + if i == excludeCrew { + continue + } + if len(c.Path) == 0 && c.TileX == cell[0] && c.TileY == cell[1] { + return true + } + } + return false +} + // Draw renders the whole frame. func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{10, 15, 25, 255}) @@ -123,6 +140,7 @@ func (g *Game) Layout(_, _ int) (int, int) { func main() { ship := NewPlayerShip() walk := walkableTiles(ship) + cells := cellSet(ship) crew := NewStartingCrew() systems := NewStartingSystems() combat := NewCombat() @@ -133,6 +151,7 @@ func main() { if err := ebiten.RunGame(&Game{ ship: ship, walk: walk, + cells: cells, crew: crew, selectedCrew: -1, systems: systems, diff --git a/render.go b/render.go index b4ba8310d42895100a665b513a3ec1cb4bb12724..2f1eb9708612516eae7feef1b4b678f351a39341 100644 --- a/render.go +++ b/render.go @@ -111,6 +111,13 @@ func drawRoom(screen *ebiten.Image, r Room, powered bool) { // Label: a few pixels inside the top-left corner. ebitenutil.DebugPrintAt(screen, r.Name, int(x)+3, int(y)+2) + + // Cell markers: small dim squares at cell positions. + for _, c := range r.Cells { + cx := float32(c[0]*TilePx + TilePx/2 - 2) + cy := float32(c[1]*TilePx + TilePx/2 - 2) + vector.DrawFilledRect(screen, cx, cy, 4, 4, color.RGBA{255, 255, 255, 60}, false) + } } // drawHullBars renders hull readouts for both sides. diff --git a/ship.go b/ship.go index 35d5338bc84c73a75093c7992128141bfdf3cb24..b7b805146b27d695996be2ea5fc36fdcaf9c32e6 100644 --- a/ship.go +++ b/ship.go @@ -24,6 +24,7 @@ type Room struct { GridW int GridH int Role RoomRole + Cells [][2]int } // Ship is a pure-data description of a ship layout. @@ -39,6 +40,11 @@ func NewEnemyShip() Ship { rooms := make([]Room, len(player.Rooms)) for i, r := range player.Rooms { r.GridX += 24 + shifted := make([][2]int, len(r.Cells)) + for j, c := range r.Cells { + shifted[j] = [2]int{c[0] + 24, c[1]} + } + r.Cells = shifted rooms[i] = r } return Ship{Rooms: rooms} @@ -50,11 +56,16 @@ func NewEnemyShip() Ship { func NewPlayerShip() Ship { return Ship{ Rooms: []Room{ - {ID: 1, Name: "Pilot", GridX: 3, GridY: 9, GridW: 3, GridH: 3, Role: RolePilot}, - {ID: 2, Name: "Weapons", GridX: 6, GridY: 6, GridW: 4, GridH: 3, Role: RoleWeapons}, - {ID: 3, Name: "Shields", GridX: 6, GridY: 9, GridW: 4, GridH: 3, Role: RoleShields}, - {ID: 4, Name: "MedBay", GridX: 6, GridY: 12, GridW: 4, GridH: 3, Role: RoleMedBay}, - {ID: 5, Name: "Engines", GridX: 10, GridY: 9, GridW: 3, GridH: 3, Role: RoleEngines}, + {ID: 1, Name: "Pilot", GridX: 3, GridY: 9, GridW: 3, GridH: 3, Role: RolePilot, + Cells: [][2]int{{4, 10}, {5, 10}}}, + {ID: 2, Name: "Weapons", GridX: 6, GridY: 6, GridW: 4, GridH: 3, Role: RoleWeapons, + Cells: [][2]int{{7, 7}, {8, 7}}}, + {ID: 3, Name: "Shields", GridX: 6, GridY: 9, GridW: 4, GridH: 3, Role: RoleShields, + Cells: [][2]int{{7, 10}, {8, 10}}}, + {ID: 4, Name: "MedBay", GridX: 6, GridY: 12, GridW: 4, GridH: 3, Role: RoleMedBay, + Cells: [][2]int{{7, 13}, {8, 13}}}, + {ID: 5, Name: "Engines", GridX: 10, GridY: 9, GridW: 3, GridH: 3, Role: RoleEngines, + Cells: [][2]int{{11, 10}, {12, 10}}}, }, } } diff --git a/tiles.go b/tiles.go index d0a346836acca1dc5a58aa14ba576d8dcb7746b4..55f959de36f1c873892531d0f59a4a8a9367a525 100644 --- a/tiles.go +++ b/tiles.go @@ -12,6 +12,16 @@ func walkableTiles(s Ship) map[[2]int]bool { return walk } +func cellSet(s Ship) map[[2]int]bool { + cells := make(map[[2]int]bool) + for _, r := range s.Rooms { + for _, c := range r.Cells { + cells[c] = true + } + } + return cells +} + func bfsPath(walk map[[2]int]bool, from, to [2]int) [][2]int { if from == to { return nil diff --git a/tiles_test.go b/tiles_test.go index 52848fe7c0a9a275c9fcf895601d6401639af549..1b3ec2b69e707fa49450506f2b71c91ebb5dc33a 100644 --- a/tiles_test.go +++ b/tiles_test.go @@ -41,6 +41,49 @@ func TestWalkableTiles_ignoresOutside(t *testing.T) { } } +func TestCellSet_returnsAllCells(t *testing.T) { + s := NewPlayerShip() + cells := cellSet(s) + if len(cells) != 10 { + t.Fatalf("expected 10 cells (2 per room × 5 rooms), got %d", len(cells)) + } + expected := [][2]int{ + {4, 10}, {5, 10}, + {7, 7}, {8, 7}, + {7, 10}, {8, 10}, + {7, 13}, {8, 13}, + {11, 10}, {12, 10}, + } + for _, c := range expected { + if !cells[c] { + t.Errorf("expected cell %v to be present", c) + } + } +} + +func TestCellSet_allWalkable(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + cells := cellSet(s) + for c := range cells { + if !walk[c] { + t.Errorf("cell %v is not walkable", c) + } + } +} + +func TestCellSet_startingCrewOnCells(t *testing.T) { + s := NewPlayerShip() + cells := cellSet(s) + crew := NewStartingCrew() + for _, c := range crew { + pos := [2]int{c.TileX, c.TileY} + if !cells[pos] { + t.Errorf("crew %s at %v is not on a cell", c.Name, pos) + } + } +} + func TestBFSPath_sameTile(t *testing.T) { s := NewPlayerShip() walk := walkableTiles(s)