M combat_test.go => combat_test.go +1 -1
@@ 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 {
M main.go => main.go +21 -2
@@ 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,
M render.go => render.go +7 -0
@@ 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.
M ship.go => ship.go +16 -5
@@ 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}}},
},
}
}
M tiles.go => tiles.go +10 -0
@@ 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
M tiles_test.go => tiles_test.go +43 -0
@@ 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)