fix(game): gate group visibility by visibility range, report battle classes #78
@@ -135,7 +135,7 @@ func (c *Cache) ReportRace(ri int, rep *mr.Report, battles []*mr.BattleReport, b
|
|||||||
|
|
||||||
// ship classes
|
// ship classes
|
||||||
c.ReportLocalShipClass(ri, rep)
|
c.ReportLocalShipClass(ri, rep)
|
||||||
c.ReportOtherShipClass(ri, rep)
|
c.ReportOtherShipClass(ri, rep, battles)
|
||||||
|
|
||||||
// battles
|
// battles
|
||||||
c.ReportBattle(ri, rep, battles)
|
c.ReportBattle(ri, rep, battles)
|
||||||
@@ -249,7 +249,7 @@ func (c *Cache) ReportLocalShipClass(ri int, report *mr.Report) {
|
|||||||
slices.SortFunc(report.LocalShipClass, func(a, b mr.ShipClass) int { return cmp.Compare(a.Name, b.Name) })
|
slices.SortFunc(report.LocalShipClass, func(a, b mr.ShipClass) int { return cmp.Compare(a.Name, b.Name) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) ReportOtherShipClass(ri int, rep *mr.Report) {
|
func (c *Cache) ReportOtherShipClass(ri int, rep *mr.Report, battles []*mr.BattleReport) {
|
||||||
c.validateRaceIndex(ri)
|
c.validateRaceIndex(ri)
|
||||||
r := &c.g.Race[ri]
|
r := &c.g.Race[ri]
|
||||||
|
|
||||||
@@ -273,26 +273,46 @@ func (c *Cache) ReportOtherShipClass(ri int, rep *mr.Report) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// add visible ship classes from battles
|
// Ship classes seen in battles the recipient took part in or witnessed.
|
||||||
// for bi := range battle {
|
// The battle report carries the class name and owner race; the class
|
||||||
// for si := range battle[bi].Ships {
|
// design is looked up from that race's ship types, which stay present in
|
||||||
// g := battle[bi].Ships[si]
|
// the state even though the groups themselves are deleted before reports
|
||||||
// if skip(g.OwnerID, g.ClassName) {
|
// are generated.
|
||||||
// continue
|
for bi := range battles {
|
||||||
// }
|
br := battles[bi]
|
||||||
|
visible := false
|
||||||
// sliceIndexValidate(&rep.OtherShipClass, i)
|
for k := range br.Races {
|
||||||
// rep.OtherShipClass[i].Race = c.g.Race[c.RaceIndex(g.OwnerID)].Name
|
if br.Races[k] == r.ID {
|
||||||
// rep.OtherShipClass[i].Name = g.ClassName
|
visible = true
|
||||||
// rep.OtherShipClass[i].Drive = g.DriveTech
|
break
|
||||||
// rep.OtherShipClass[i].Armament = g.ClassArmament
|
}
|
||||||
// rep.OtherShipClass[i].Weapons = g.WeaponsTech
|
}
|
||||||
// rep.OtherShipClass[i].Shields = g.ShieldsTech
|
if !visible {
|
||||||
// rep.OtherShipClass[i].Cargo = g.CargoTech
|
continue
|
||||||
// rep.OtherShipClass[i].Mass = g.ClassMass
|
}
|
||||||
// i++
|
for si := range br.Ships {
|
||||||
// }
|
bg := br.Ships[si]
|
||||||
// }
|
ownerIdx, err := c.raceIndex(bg.Race)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ownerID := c.g.Race[ownerIdx].ID
|
||||||
|
st, _, ok := c.ShipClass(ownerIdx, bg.ClassName)
|
||||||
|
if !ok || skip(ownerID, st.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sliceIndexValidate(&rep.OtherShipClass, i)
|
||||||
|
rep.OtherShipClass[i].Race = bg.Race
|
||||||
|
rep.OtherShipClass[i].Name = st.Name
|
||||||
|
rep.OtherShipClass[i].Drive = mr.F(st.Drive.F())
|
||||||
|
rep.OtherShipClass[i].Armament = st.Armament
|
||||||
|
rep.OtherShipClass[i].Weapons = mr.F(st.Weapons.F())
|
||||||
|
rep.OtherShipClass[i].Shields = mr.F(st.Shields.F())
|
||||||
|
rep.OtherShipClass[i].Cargo = mr.F(st.Cargo.F())
|
||||||
|
rep.OtherShipClass[i].Mass = mr.F(st.EmptyMass())
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// add visible ships from owned and observed planets
|
// add visible ships from owned and observed planets
|
||||||
for pn := range rep.OnPlanetGroupCache {
|
for pn := range rep.OnPlanetGroupCache {
|
||||||
@@ -397,6 +417,22 @@ func (c *Cache) ReportIncomingGroup(ri int, rep *mr.Report) {
|
|||||||
if !p2.OwnedBy(r.ID) {
|
if !p2.OwnedBy(r.ID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Beyond the visibility range (driveTech*30) of every owned planet the
|
||||||
|
// group is not shown at all, even though it heads to one of them.
|
||||||
|
visible := false
|
||||||
|
for pi := range c.g.Map.Planet {
|
||||||
|
op := &c.g.Map.Planet[pi]
|
||||||
|
if !op.OwnedBy(r.ID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if d, ok := rep.InSpaceGroupRangeCache[sgi][op.Number]; ok && d <= r.VisibilityDistance() {
|
||||||
|
visible = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
||||||
var speed, mass float64
|
var speed, mass float64
|
||||||
@@ -685,34 +721,45 @@ func (c *Cache) ReportOtherGroup(ri int, rep *mr.Report) {
|
|||||||
func (c *Cache) ReportUnidentifiedGroup(ri int, rep *mr.Report) {
|
func (c *Cache) ReportUnidentifiedGroup(ri int, rep *mr.Report) {
|
||||||
c.validateRaceIndex(ri)
|
c.validateRaceIndex(ri)
|
||||||
r := &c.g.Race[ri]
|
r := &c.g.Race[ri]
|
||||||
flightDistance := r.FlightDistance()
|
visibility := r.VisibilityDistance()
|
||||||
|
|
||||||
clear(rep.UnidentifiedGroup)
|
clear(rep.UnidentifiedGroup)
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for sgi := range rep.InSpaceGroupRangeCache {
|
for sgi := range rep.InSpaceGroupRangeCache {
|
||||||
sg := c.ShipGroup(sgi)
|
sg := c.ShipGroup(sgi)
|
||||||
if sg.OwnerID == rep.RaceID {
|
if sg.OwnerID == r.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if sg.StateInSpace == nil {
|
if sg.StateInSpace == nil {
|
||||||
panic(fmt.Sprintf("pre-calculated distance group not in space: i=%d", sgi))
|
panic(fmt.Sprintf("pre-calculated distance group not in space: i=%d", sgi))
|
||||||
}
|
}
|
||||||
|
// Groups heading to one of the recipient's planets are listed in full
|
||||||
|
// under "incoming groups"; the unidentified list is for the rest.
|
||||||
|
if c.MustPlanet(sg.Destination).OwnedBy(r.ID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Shown once, and only within the visibility range (driveTech*30) of at
|
||||||
|
// least one of the recipient's planets.
|
||||||
|
visible := false
|
||||||
for pi := range c.g.Map.Planet {
|
for pi := range c.g.Map.Planet {
|
||||||
p := &c.g.Map.Planet[pi]
|
p := &c.g.Map.Planet[pi]
|
||||||
if !p.OwnedBy(r.ID) {
|
if !p.OwnedBy(r.ID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if v, ok := rep.InSpaceGroupRangeCache[sgi][p.Number]; !ok {
|
if v, ok := rep.InSpaceGroupRangeCache[sgi][p.Number]; ok && v <= visibility {
|
||||||
panic(fmt.Sprintf("distance cache not pre-calculated: i=%d p=#%d", sgi, p.Number))
|
visible = true
|
||||||
} else if v <= flightDistance {
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
continue
|
||||||
|
}
|
||||||
sliceIndexValidate(&rep.UnidentifiedGroup, i)
|
sliceIndexValidate(&rep.UnidentifiedGroup, i)
|
||||||
rep.UnidentifiedGroup[i].X = mr.F(sg.StateInSpace.X.F())
|
rep.UnidentifiedGroup[i].X = mr.F(sg.StateInSpace.X.F())
|
||||||
rep.UnidentifiedGroup[i].Y = mr.F(sg.StateInSpace.Y.F())
|
rep.UnidentifiedGroup[i].Y = mr.F(sg.StateInSpace.Y.F())
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) otherGroup(v *mr.OtherGroup, sg *game.ShipGroup, st *game.ShipType) {
|
func (c *Cache) otherGroup(v *mr.OtherGroup, sg *game.ShipGroup, st *game.ShipType) {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package controller_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"galaxy/game/internal/model/game"
|
||||||
"galaxy/model/report"
|
"galaxy/model/report"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,3 +88,80 @@ func TestReportLocalShipClass(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReportIncomingGroupVisibility checks that a group heading to one of the
|
||||||
|
// recipient's planets is reported only while within the recipient's visibility
|
||||||
|
// range (driveTech*30); beyond it the group is hidden even though it is inbound.
|
||||||
|
func TestReportIncomingGroupVisibility(t *testing.T) {
|
||||||
|
c, _ := newCache()
|
||||||
|
gi := c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R1_Planet_1_num, 5)
|
||||||
|
c.ShipGroup(gi).Destination = R0_Planet_0_num
|
||||||
|
|
||||||
|
// Within Race_0 visibility (driveTech 1.1 -> 33 ly), near Planet_2 (3,3).
|
||||||
|
c.ShipGroup(gi).StateInSpace = &game.InSpace{Origin: R1_Planet_1_num, X: floatRef(5), Y: floatRef(5)}
|
||||||
|
rep := c.InitReport(1)
|
||||||
|
c.ReportIncomingGroup(Race_0_idx, rep)
|
||||||
|
assert.Len(t, rep.IncomingGroup, 1)
|
||||||
|
|
||||||
|
// Beyond the visibility of every Race_0 planet: hidden.
|
||||||
|
c.ShipGroup(gi).StateInSpace = &game.InSpace{Origin: R1_Planet_1_num, X: floatRef(40), Y: floatRef(40)}
|
||||||
|
rep = c.InitReport(1)
|
||||||
|
c.ReportIncomingGroup(Race_0_idx, rep)
|
||||||
|
assert.Len(t, rep.IncomingGroup, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportUnidentifiedGroup checks the three rules for the unidentified list:
|
||||||
|
// groups heading to the recipient's planets are excluded (they are "incoming"),
|
||||||
|
// only groups within visibility (driveTech*30) appear, and each group appears
|
||||||
|
// once even when several owned planets are in range.
|
||||||
|
func TestReportUnidentifiedGroup(t *testing.T) {
|
||||||
|
c, _ := newCache()
|
||||||
|
cls := c.MustShipClass(Race_1_idx, Race_1_Gunship).ID
|
||||||
|
|
||||||
|
// Not inbound to Race_0, within visibility of BOTH Planet_0 and Planet_2.
|
||||||
|
g0 := c.CreateShipsUnsafe_T(Race_1_idx, cls, R1_Planet_1_num, 1)
|
||||||
|
c.ShipGroup(g0).Destination = Uninhabited_Planet_3_num
|
||||||
|
c.ShipGroup(g0).StateInSpace = &game.InSpace{Origin: R1_Planet_1_num, X: floatRef(5), Y: floatRef(5)}
|
||||||
|
|
||||||
|
// Inbound to a Race_0 planet -> reported as incoming, not unidentified.
|
||||||
|
g1 := c.CreateShipsUnsafe_T(Race_1_idx, cls, R1_Planet_1_num, 1)
|
||||||
|
c.ShipGroup(g1).Destination = R0_Planet_0_num
|
||||||
|
c.ShipGroup(g1).StateInSpace = &game.InSpace{Origin: R1_Planet_1_num, X: floatRef(5), Y: floatRef(5)}
|
||||||
|
|
||||||
|
// Not inbound, beyond visibility -> hidden.
|
||||||
|
g2 := c.CreateShipsUnsafe_T(Race_1_idx, cls, R1_Planet_1_num, 1)
|
||||||
|
c.ShipGroup(g2).Destination = Uninhabited_Planet_3_num
|
||||||
|
c.ShipGroup(g2).StateInSpace = &game.InSpace{Origin: R1_Planet_1_num, X: floatRef(40), Y: floatRef(40)}
|
||||||
|
|
||||||
|
rep := c.InitReport(1)
|
||||||
|
c.ReportUnidentifiedGroup(Race_0_idx, rep)
|
||||||
|
assert.Len(t, rep.UnidentifiedGroup, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportOtherShipClassFromBattle checks that the class of a foreign ship
|
||||||
|
// met in a battle the recipient witnessed is surfaced in OtherShipClass, with
|
||||||
|
// its design looked up from the owner race's ship types, while the recipient's
|
||||||
|
// own class is skipped.
|
||||||
|
func TestReportOtherShipClassFromBattle(t *testing.T) {
|
||||||
|
c, _ := newCache()
|
||||||
|
br := &report.BattleReport{
|
||||||
|
Races: map[int]uuid.UUID{0: Race_0.ID, 1: Race_1.ID},
|
||||||
|
Ships: map[int]report.BattleReportGroup{
|
||||||
|
0: {Race: Race_1.Name, ClassName: Race_1_Gunship},
|
||||||
|
1: {Race: Race_0.Name, ClassName: Race_0_Gunship}, // recipient's own -> skipped
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rep := c.InitReport(1)
|
||||||
|
c.ReportOtherShipClass(Race_0_idx, rep, []*report.BattleReport{br})
|
||||||
|
|
||||||
|
assert.Len(t, rep.OtherShipClass, 1)
|
||||||
|
g := rep.OtherShipClass[0]
|
||||||
|
assert.Equal(t, Race_1.Name, g.Race)
|
||||||
|
assert.Equal(t, Race_1_Gunship, g.Name)
|
||||||
|
assert.Equal(t, report.F(60.), g.Drive)
|
||||||
|
assert.Equal(t, uint(3), g.Armament)
|
||||||
|
assert.Equal(t, report.F(30.), g.Weapons)
|
||||||
|
assert.Equal(t, report.F(100.), g.Shields)
|
||||||
|
assert.Equal(t, report.F(0.), g.Cargo)
|
||||||
|
assert.Equal(t, report.F(220.), g.Mass)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user