233c9ebc2a
feat: order commands result save/load
648 lines
18 KiB
Go
648 lines
18 KiB
Go
package controller
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"iter"
|
|
"maps"
|
|
"math/rand/v2"
|
|
"slices"
|
|
|
|
"github.com/google/uuid"
|
|
e "github.com/iliadenisov/galaxy/internal/error"
|
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
|
"github.com/iliadenisov/galaxy/internal/number"
|
|
"github.com/iliadenisov/galaxy/internal/util"
|
|
)
|
|
|
|
// ShipGroup is a proxy func, nothing to cache
|
|
func (c *Cache) ShipGroup(groupIndex int) *game.ShipGroup {
|
|
c.validateShipGroupIndex(groupIndex)
|
|
return &c.g.ShipGroups[groupIndex]
|
|
}
|
|
|
|
func (c *Cache) internalShipGroupJoinFleet(groupIndex int, fID uuid.UUID) {
|
|
c.validateShipGroupIndex(groupIndex)
|
|
c.g.ShipGroups[groupIndex].FleetID = &fID
|
|
c.invalidateFleetCache()
|
|
}
|
|
|
|
func (c *Cache) ShipGroupShipsNumber(groupIndex int, number uint) {
|
|
c.validateShipGroupIndex(groupIndex)
|
|
if c.g.ShipGroups[groupIndex].Number > 0 {
|
|
c.g.ShipGroups[groupIndex].Load = game.F(c.g.ShipGroups[groupIndex].Load.F() / float64(c.g.ShipGroups[groupIndex].Number) * float64(number))
|
|
}
|
|
c.g.ShipGroups[groupIndex].Number = number
|
|
}
|
|
|
|
func (c *Cache) ShipGroupsIndex() iter.Seq[int] {
|
|
return func(yield func(int) bool) {
|
|
for i := range c.g.ShipGroups {
|
|
if !yield(i) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) ShipGroupOwnerRaceIndex(groupIndex int) int {
|
|
c.validateShipGroupIndex(groupIndex)
|
|
if len(c.cacheRaceIndexByShipGroupIndex) == 0 {
|
|
c.cacheShipsAndGroups()
|
|
}
|
|
if v, ok := c.cacheRaceIndexByShipGroupIndex[groupIndex]; ok {
|
|
return v
|
|
} else {
|
|
panic(fmt.Sprintf("ShipGroupRace: group not found by index=%v", groupIndex))
|
|
}
|
|
}
|
|
|
|
func (c *Cache) ShipGroupOwnerRace(groupIndex int) *game.Race {
|
|
return &c.g.Race[c.ShipGroupOwnerRaceIndex(groupIndex)]
|
|
}
|
|
|
|
func (c *Cache) ShipGroupDestroyItem(i int) {
|
|
c.validateShipGroupIndex(i)
|
|
sg := &c.g.ShipGroups[i]
|
|
if sg.Number == 0 {
|
|
panic("group has no ships")
|
|
}
|
|
sg.Load = game.F(sg.Load.F() / float64(sg.Number) * float64(sg.Number-1))
|
|
sg.Number -= 1
|
|
}
|
|
|
|
func (c *Cache) DeleteKilledShipGroups() {
|
|
keepFleet := make(map[uuid.UUID]bool, len(c.g.Fleets))
|
|
for sgi := len(c.g.ShipGroups) - 1; sgi >= 0; sgi-- {
|
|
if c.g.ShipGroups[sgi].FleetID != nil {
|
|
id := *c.g.ShipGroups[sgi].FleetID
|
|
keepFleet[id] = keepFleet[id] || c.g.ShipGroups[sgi].Number > 0
|
|
}
|
|
if c.g.ShipGroups[sgi].Number == 0 {
|
|
c.g.ShipGroups = append(c.g.ShipGroups[:sgi], c.g.ShipGroups[sgi+1:]...)
|
|
}
|
|
}
|
|
c.invalidateShipGroupCache()
|
|
for id, keep := range keepFleet {
|
|
if keep {
|
|
continue
|
|
}
|
|
c.unsafeDeleteFleet(c.MustFleetIndex(id))
|
|
}
|
|
}
|
|
|
|
func (c *Cache) TurnMergeEqualShipGroups() {
|
|
for i := range c.listRaceActingIdx() {
|
|
c.transferPendingGroups(i)
|
|
c.shipGroupMerge(i)
|
|
}
|
|
}
|
|
|
|
func (c *Cache) transferPendingGroups(ri int) {
|
|
c.validateRaceIndex(ri)
|
|
for sg := range c.listShipGroups(ri) {
|
|
if sg.State() == game.StateTransfer {
|
|
sg.StateTransfer = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// shipGroupMerge merges several equal ship groups into one
|
|
func (c *Cache) shipGroupMerge(ri int) {
|
|
c.validateRaceIndex(ri)
|
|
raceGroups := make([]game.ShipGroup, 0)
|
|
for sg := range c.listShipGroups(ri) {
|
|
raceGroups = append(raceGroups, *sg)
|
|
}
|
|
|
|
origin := len(raceGroups)
|
|
if origin < 2 {
|
|
return
|
|
}
|
|
for i := 0; i < len(raceGroups)-1; i++ {
|
|
for j := len(raceGroups) - 1; j > i; j-- {
|
|
if raceGroups[i].Equal(raceGroups[j]) {
|
|
raceGroups[i].ID = raceGroups[j].ID // resulting group will have latest ID
|
|
raceGroups[i].Number += raceGroups[j].Number
|
|
raceGroups = append(raceGroups[:j], raceGroups[j+1:]...)
|
|
}
|
|
}
|
|
}
|
|
if len(raceGroups) == origin {
|
|
return
|
|
}
|
|
|
|
toDelete := make([]int, 0)
|
|
for i := range c.ShipGroupsIndex() {
|
|
if c.ShipGroup(i).OwnerID == c.g.Race[ri].ID {
|
|
toDelete = append(toDelete, i)
|
|
}
|
|
}
|
|
|
|
slices.Sort(toDelete)
|
|
slices.Reverse(toDelete)
|
|
for _, sgi := range toDelete {
|
|
c.unsafeDeleteShipGroup(sgi)
|
|
}
|
|
|
|
for i := range raceGroups {
|
|
c.appendShipGroup(ri, &raceGroups[i])
|
|
}
|
|
}
|
|
|
|
func (c *Cache) shipGroupDismantle(ri int, groupIndex uuid.UUID) error {
|
|
c.validateRaceIndex(ri)
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group #%d", groupIndex)
|
|
}
|
|
|
|
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
|
return e.NewShipsBusyError("state: %s", state)
|
|
}
|
|
|
|
pl, ok := c.Planet(c.ShipGroup(sgi).Destination)
|
|
if !ok {
|
|
return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
|
|
}
|
|
p := *pl
|
|
|
|
st := c.ShipGroupShipClass(sgi)
|
|
|
|
if c.ShipGroup(sgi).CargoType != nil {
|
|
ct := *c.ShipGroup(sgi).CargoType
|
|
load := c.ShipGroup(sgi).Load.F()
|
|
switch ct {
|
|
case game.CargoColonist:
|
|
if p.OwnedBy(c.g.Race[ri].ID) {
|
|
p = game.UnloadColonists(p, load)
|
|
}
|
|
case game.CargoMaterial:
|
|
p.Material = p.Material.Add(load)
|
|
case game.CargoCapital:
|
|
p.Capital = p.Capital.Add(load)
|
|
}
|
|
}
|
|
|
|
p.Material = p.Material.Add(c.ShipGroup(sgi).EmptyMass(st))
|
|
|
|
c.unsafeDeleteShipGroup(sgi)
|
|
|
|
c.g.Map.Planet[c.MustPlanetIndex(p.Number)] = p
|
|
|
|
return nil
|
|
}
|
|
|
|
// Корабль может нести только один тип груза одновременно.
|
|
// Возможные типы груза - это колонисты, сырье и промышленность.
|
|
// Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется.
|
|
// Указанное количество груза равномерно распределяется между всеми кораблями группы.
|
|
func (c *Cache) shipGroupLoad(ri int, groupID uuid.UUID, ct game.CargoType, quantity float64) error {
|
|
c.validateRaceIndex(ri)
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group %s", groupID)
|
|
}
|
|
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
|
return e.NewShipsBusyError("state: %s", state)
|
|
}
|
|
p, ok := c.Planet(c.ShipGroup(sgi).Destination)
|
|
if !ok {
|
|
return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
|
|
}
|
|
|
|
if p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
|
|
return e.NewEntityNotOwnedError("planet #%d", p.Number)
|
|
}
|
|
st := c.ShipGroupShipClass(sgi)
|
|
|
|
if st.Cargo < 1 {
|
|
return e.NewNoCargoBayError("ship_type %q", st.Name)
|
|
}
|
|
if c.ShipGroup(sgi).CargoType != nil && *c.ShipGroup(sgi).CargoType != ct {
|
|
return e.NewCargoLoadNotEqualError("cargo: %v", *c.ShipGroup(sgi).CargoType)
|
|
}
|
|
|
|
capacity := c.ShipGroup(sgi).CargoCapacity(st)
|
|
freeShipGroupCargoLoad := capacity - float64(c.ShipGroup(sgi).Load)
|
|
if freeShipGroupCargoLoad == 0 {
|
|
return e.NewCargoLoadNoSpaceLeftError()
|
|
}
|
|
var availableOnPlanet *game.Float
|
|
switch ct {
|
|
case game.CargoMaterial:
|
|
availableOnPlanet = &p.Material
|
|
case game.CargoCapital:
|
|
availableOnPlanet = &p.Capital
|
|
case game.CargoColonist:
|
|
availableOnPlanet = &p.Colonists
|
|
default:
|
|
return e.NewGameStateError("CargoType not accepted: %v", ct)
|
|
}
|
|
if quantity > float64(*availableOnPlanet) || *availableOnPlanet == 0 {
|
|
return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", p.Number, ct, *availableOnPlanet)
|
|
}
|
|
toBeLoaded := quantity
|
|
if quantity == 0 {
|
|
toBeLoaded = float64(*availableOnPlanet)
|
|
}
|
|
if toBeLoaded > freeShipGroupCargoLoad {
|
|
toBeLoaded = freeShipGroupCargoLoad
|
|
}
|
|
*availableOnPlanet = (*availableOnPlanet).Add(-toBeLoaded)
|
|
c.ShipGroup(sgi).Load = c.ShipGroup(sgi).Load.Add(toBeLoaded)
|
|
if c.ShipGroup(sgi).Load > 0 {
|
|
c.ShipGroup(sgi).CargoType = &ct
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Промышленность и Сырье могут быть выгружены на любой планете.
|
|
// Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты.
|
|
func (c *Cache) shipGroupUnload(ri int, groupID uuid.UUID, quantity float64) error {
|
|
c.validateRaceIndex(ri)
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group %s", groupID)
|
|
}
|
|
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
|
return e.NewShipsBusyError("state: %s", state)
|
|
}
|
|
st := c.ShipGroupShipClass(sgi)
|
|
if st.Cargo < 1 {
|
|
return e.NewNoCargoBayError("ship_type %q", st.Name)
|
|
}
|
|
if c.ShipGroup(sgi).CargoType == nil || c.ShipGroup(sgi).Load == 0 {
|
|
return e.NewCargoUnloadEmptyError()
|
|
}
|
|
|
|
ct := *c.ShipGroup(sgi).CargoType
|
|
p := c.MustPlanet(c.ShipGroup(sgi).Destination)
|
|
|
|
if ct == game.CargoColonist && p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
|
|
return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct)
|
|
}
|
|
|
|
toBeUnloaded := quantity
|
|
if quantity == 0 {
|
|
toBeUnloaded = float64(c.ShipGroup(sgi).Load)
|
|
}
|
|
if toBeUnloaded > float64(c.ShipGroup(sgi).Load) {
|
|
return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load)
|
|
}
|
|
|
|
c.unsafeUnloadCargo(sgi, toBeUnloaded)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) shipGroupUnloadColonistChallenge(groupIDs []uuid.UUID, quantites []float64) error {
|
|
if len(groupIDs) != len(quantites) {
|
|
e.NewGameStateError("challenge group unload: groups=%d quantities=%d", len(groupIDs), len(quantites))
|
|
}
|
|
if len(groupIDs) == 0 {
|
|
return nil
|
|
}
|
|
type challenger struct {
|
|
ri int
|
|
sgi int
|
|
quantity float64
|
|
}
|
|
challenge := make(map[uint]map[int][]challenger, 0)
|
|
for i, gid := range groupIDs {
|
|
sgi, ok := c.shipGroupIndexByID(gid)
|
|
if !ok {
|
|
return e.NewGameStateError("challenge group unload: group not found: %v", gid)
|
|
}
|
|
sg := c.ShipGroup(sgi)
|
|
pn, ok := sg.AtPlanet()
|
|
if !ok || sg.CargoType == nil {
|
|
continue
|
|
}
|
|
p := c.MustPlanet(pn)
|
|
ri := c.ShipGroupOwnerRaceIndex(sgi)
|
|
if p.Owned() || *sg.CargoType != game.CargoColonist {
|
|
if err := c.shipGroupUnload(ri, gid, quantites[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, ok := challenge[pn]; !ok {
|
|
challenge[pn] = make(map[int][]challenger)
|
|
}
|
|
challenge[pn][ri] = append(challenge[pn][ri], challenger{ri: ri, sgi: sgi, quantity: quantites[i]})
|
|
}
|
|
for pn := range challenge {
|
|
if len(challenge[pn]) < 2 {
|
|
for _, v := range challenge[pn] {
|
|
for _, ch := range v {
|
|
if err := c.shipGroupUnload(ch.ri, c.ShipGroup(ch.sgi).ID, ch.quantity); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
sum := make(map[int]float64)
|
|
races := slices.Collect(maps.Keys(challenge[pn]))
|
|
for ri, groups := range challenge[pn] {
|
|
for i := range groups {
|
|
sum[ri] = sum[ri] + groups[i].quantity
|
|
}
|
|
c.listProducingPlanets()
|
|
}
|
|
|
|
slices.SortFunc(races, func(ria, rib int) int {
|
|
return cmp.Or(
|
|
// Наибольшее количество выгружаемых колонистов
|
|
cmp.Compare(sum[rib], sum[ria]),
|
|
|
|
// Наибольшее количество населения расы (они же голоса)
|
|
cmp.Compare(c.g.Race[rib].Votes, c.g.Race[ria].Votes),
|
|
|
|
// Случайный выбор претендента
|
|
cmp.Compare(rand.Float64(), rand.Float64()),
|
|
|
|
// in theoty, unreacheable option, but let's randomize again
|
|
cmp.Compare(rand.Float64(), rand.Float64()),
|
|
)
|
|
})
|
|
|
|
for _, ch := range challenge[pn][races[0]] {
|
|
if err := c.shipGroupUnload(ch.ri, c.ShipGroup(ch.sgi).ID, ch.quantity); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) shipGroupIndexByID(id uuid.UUID) (int, bool) {
|
|
for sgi := range c.g.ShipGroups {
|
|
if c.g.ShipGroups[sgi].ID == id {
|
|
return sgi, true
|
|
}
|
|
}
|
|
return -1, false
|
|
}
|
|
|
|
func (c *Cache) unsafeUnloadCargo(sgi int, q float64) {
|
|
if q <= 0 {
|
|
return
|
|
}
|
|
if st := c.ShipGroup(sgi).State(); st != game.StateInOrbit {
|
|
panic(fmt.Sprintf("invalid group state: %v", st))
|
|
}
|
|
c.validateShipGroupIndex(sgi)
|
|
p := c.MustPlanet(c.ShipGroup(sgi).Destination)
|
|
ct := *c.ShipGroup(sgi).CargoType
|
|
|
|
var availableOnPlanet *game.Float
|
|
switch ct {
|
|
case game.CargoColonist:
|
|
availableOnPlanet = &p.Colonists
|
|
if !p.Owned() {
|
|
p.Own(c.ShipGroup(sgi).OwnerID)
|
|
p.Production = game.ProductionCapital.AsType(uuid.Nil)
|
|
}
|
|
case game.CargoMaterial:
|
|
availableOnPlanet = &p.Material
|
|
case game.CargoCapital:
|
|
availableOnPlanet = &p.Capital
|
|
}
|
|
*availableOnPlanet = (*availableOnPlanet).Add(q)
|
|
|
|
c.ShipGroup(sgi).Load = c.ShipGroup(sgi).Load.Add(-q)
|
|
if c.ShipGroup(sgi).Load == 0 {
|
|
c.ShipGroup(sgi).CargoType = nil
|
|
}
|
|
|
|
p.UnpackColonists()
|
|
p.UnpackCapital()
|
|
}
|
|
|
|
func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err error) {
|
|
c.validateRaceIndex(ri)
|
|
if ri == riAccept {
|
|
return e.NewSameRaceError(c.g.Race[riAccept].Name)
|
|
}
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group %s", groupID)
|
|
}
|
|
sg := c.ShipGroup(sgi)
|
|
state := sg.State()
|
|
if state == game.StateTransfer {
|
|
return e.NewShipsBusyError("state: %s", state)
|
|
}
|
|
|
|
st := c.ShipGroupShipClass(sgi)
|
|
|
|
var stAcc int
|
|
var name = st.Name
|
|
if stAcc = slices.IndexFunc(c.g.Race[riAccept].ShipTypes, func(v game.ShipType) bool { return v.Name == st.Name }); stAcc >= 0 &&
|
|
!st.Equal(c.g.Race[riAccept].ShipTypes[stAcc]) {
|
|
name = util.AppendRandomSuffix(name)
|
|
}
|
|
if stAcc < 0 || name != st.Name {
|
|
err = c.ShipClassCreate(riAccept,
|
|
name,
|
|
st.Drive.F(),
|
|
int(st.Armament),
|
|
st.Weapons.F(),
|
|
st.Shields.F(),
|
|
st.Cargo.F())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stAcc = len(c.g.Race[riAccept].ShipTypes) - 1
|
|
}
|
|
|
|
newGroup := *(sg)
|
|
newGroup.ID = uuid.New()
|
|
newGroup.TypeID = c.g.Race[riAccept].ShipTypes[stAcc].ID
|
|
newGroup.Tech = maps.Clone(sg.Tech)
|
|
if state == game.StateLaunched {
|
|
newGroup.StateTransfer = true
|
|
}
|
|
|
|
c.appendShipGroup(riAccept, &newGroup)
|
|
c.unsafeDeleteShipGroup(sgi)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) ShipGroupBreak(ri int, groupID, newID uuid.UUID, quantity uint) (err error) {
|
|
c.validateRaceIndex(ri)
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group %s", groupID)
|
|
}
|
|
for sgi := range c.g.ShipGroups {
|
|
if c.g.ShipGroups[sgi].ID == newID {
|
|
return e.NewEntityDuplicateIdentifierError("group %s", newID)
|
|
}
|
|
}
|
|
|
|
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
|
return e.NewShipsBusyError()
|
|
}
|
|
|
|
if c.ShipGroup(sgi).Number < quantity {
|
|
return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity)
|
|
}
|
|
|
|
if quantity > 0 && quantity < c.ShipGroup(sgi).Number {
|
|
if sgi, err = c.breakGroup(ri, groupID, quantity); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
c.ShipGroup(sgi).FleetID = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) breakGroup(ri int, groupID uuid.UUID, newGroupShips uint) (int, error) {
|
|
c.validateRaceIndex(ri)
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
|
if !ok {
|
|
return -1, e.NewEntityNotExistsError("group %s", groupID)
|
|
}
|
|
if c.ShipGroup(sgi).Number < newGroupShips {
|
|
return -1, e.NewBreakGroupIllegalNumberError("group=%s ships: %d -> %d", c.ShipGroup(sgi).ID, c.ShipGroup(sgi).Number, newGroupShips)
|
|
}
|
|
return c.unsafeBreakGroup(ri, sgi, newGroupShips), nil
|
|
}
|
|
|
|
func (c *Cache) unsafeBreakGroup(ri, sgi int, newGroupShips uint) int {
|
|
newGroup := *c.ShipGroup(sgi)
|
|
if c.ShipGroup(sgi).CargoType != nil {
|
|
newGroup.Load = game.F(float64(c.ShipGroup(sgi).Load) / float64(c.ShipGroup(sgi).Number) * float64(newGroupShips))
|
|
}
|
|
newGroup.Number = newGroupShips
|
|
c.ShipGroupShipsNumber(sgi, c.ShipGroup(sgi).Number-newGroup.Number)
|
|
newGroup.FleetID = nil
|
|
return c.appendShipGroup(ri, &newGroup)
|
|
}
|
|
|
|
// Internal funcs
|
|
|
|
func (c *Cache) raceShipGroupIndex(ri int, id uuid.UUID) (int, bool) {
|
|
c.validateRaceIndex(ri)
|
|
for i := range c.ShipGroupsIndex() {
|
|
if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).ID == id {
|
|
return i, true
|
|
}
|
|
}
|
|
return -1, false
|
|
}
|
|
|
|
func (c *Cache) listShipGroupIdx(ri int) iter.Seq[int] {
|
|
c.validateRaceIndex(ri)
|
|
return func(yield func(int) bool) {
|
|
for i := range c.g.ShipGroups {
|
|
if ri == c.ShipGroupOwnerRaceIndex(i) {
|
|
if !yield(i) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) listShipGroups(ri int) iter.Seq[*game.ShipGroup] {
|
|
c.validateRaceIndex(ri)
|
|
return func(yield func(*game.ShipGroup) bool) {
|
|
for sgi := range c.listShipGroupIdx(ri) {
|
|
if !yield(c.ShipGroup(sgi)) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) shipGroupsInUpgrade(planetNumber uint) iter.Seq[*game.ShipGroup] {
|
|
return func(yield func(*game.ShipGroup) bool) {
|
|
result := make([]int, 0)
|
|
for sg := range c.g.ShipGroups {
|
|
// number checked for further sanity after battles
|
|
if c.g.ShipGroups[sg].Number > 0 && c.g.ShipGroups[sg].Destination == planetNumber && c.g.ShipGroups[sg].State() == game.StateUpgrade {
|
|
result = append(result, sg)
|
|
}
|
|
}
|
|
slices.SortFunc(result, func(a, b int) int {
|
|
return cmp.Compare(c.g.ShipGroups[b].StateUpgrade.Cost(), c.g.ShipGroups[a].StateUpgrade.Cost())
|
|
})
|
|
for i := range result {
|
|
if !yield(&c.g.ShipGroups[result[i]]) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) unsafeDeleteShipGroup(sgi int) {
|
|
c.validateShipGroupIndex(sgi)
|
|
|
|
sg := c.ShipGroup(sgi)
|
|
if sg.FleetID != nil {
|
|
fi := c.MustFleetIndex(*sg.FleetID)
|
|
fleetGroups := slices.Collect(c.fleetGroupIds(c.RaceIndex(sg.OwnerID), fi))
|
|
if len(fleetGroups) == 1 {
|
|
// remove fleet when deleting last group in the fleet
|
|
c.unsafeDeleteFleet(fi)
|
|
}
|
|
}
|
|
|
|
c.g.ShipGroups = append(c.g.ShipGroups[:sgi], c.g.ShipGroups[sgi+1:]...)
|
|
c.invalidateShipGroupCache()
|
|
}
|
|
|
|
func (c *Cache) validateShipGroupIndex(i int) {
|
|
if i >= len(c.g.ShipGroups) {
|
|
panic(fmt.Sprintf("group index out of range: %d >= %d", i, len(c.g.ShipGroups)))
|
|
}
|
|
}
|
|
|
|
func (c *Cache) unsafeCreateShips(ri int, classID uuid.UUID, planet uint, quantity uint) int {
|
|
st := c.MustShipType(ri, classID)
|
|
level := func(t game.Tech) float64 {
|
|
if t == game.TechDrive && st.DriveBlockMass() > 0 {
|
|
return number.Fixed3(c.g.Race[ri].TechLevel(game.TechDrive))
|
|
}
|
|
if t == game.TechWeapons && st.WeaponsBlockMass() > 0 {
|
|
return number.Fixed3(c.g.Race[ri].TechLevel(game.TechWeapons))
|
|
}
|
|
if t == game.TechShields && st.ShieldsBlockMass() > 0 {
|
|
return number.Fixed3(c.g.Race[ri].TechLevel(game.TechShields))
|
|
}
|
|
if t == game.TechCargo && st.CargoBlockMass() > 0 {
|
|
return number.Fixed3(c.g.Race[ri].TechLevel(game.TechCargo))
|
|
}
|
|
return 0
|
|
}
|
|
return c.appendShipGroup(ri, &game.ShipGroup{
|
|
OwnerID: c.g.Race[ri].ID,
|
|
TypeID: classID,
|
|
Destination: planet,
|
|
Number: uint(quantity),
|
|
Tech: map[game.Tech]game.Float{
|
|
game.TechDrive: game.F(level(game.TechDrive)),
|
|
game.TechWeapons: game.F(level(game.TechWeapons)),
|
|
game.TechShields: game.F(level(game.TechShields)),
|
|
game.TechCargo: game.F(level(game.TechCargo)),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (c *Cache) appendShipGroup(ri int, sg *game.ShipGroup) int {
|
|
c.validateRaceIndex(ri)
|
|
sg.ID = uuid.New()
|
|
sg.OwnerID = c.g.Race[ri].ID
|
|
sg.FleetID = nil
|
|
c.g.ShipGroups = append(c.g.ShipGroups, *sg)
|
|
i := len(c.g.ShipGroups) - 1
|
|
c.invalidateShipGroupCache()
|
|
return i
|
|
}
|