Files
galaxy-game/internal/controller/ship_group.go
T
2026-02-02 13:14:57 +02:00

602 lines
16 KiB
Go

package controller
import (
"fmt"
"iter"
"maps"
"slices"
"github.com/google/uuid"
e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/model/game"
)
func (c *Cache) CreateShips(ri int, shipTypeName string, planetNumber uint, quantity int) error {
class, _, ok := c.ShipClass(ri, shipTypeName)
if !ok {
return e.NewEntityNotExistsError("ship class q", shipTypeName)
}
p, ok := c.Planet(planetNumber)
if !ok {
return e.NewEntityNotExistsError("planet #%d", planetNumber)
}
if p.Owner != c.g.Race[ri].ID {
return e.NewEntityNotOwnedError("planet #%d", planetNumber)
}
c.createShipsUnsafe(ri, class.ID, p.Number, uint(quantity))
return nil
}
func (c *Cache) createShipsUnsafe(ri int, classID uuid.UUID, planet uint, quantity uint) {
c.appendShipGroup(ri, &game.ShipGroup{
OwnerID: c.g.Race[ri].ID,
TypeID: classID,
Destination: planet,
Number: uint(quantity),
Tech: map[game.Tech]float64{
game.TechDrive: c.g.Race[ri].TechLevel(game.TechDrive),
game.TechWeapons: c.g.Race[ri].TechLevel(game.TechWeapons),
game.TechShields: c.g.Race[ri].TechLevel(game.TechShields),
game.TechCargo: c.g.Race[ri].TechLevel(game.TechCargo),
},
})
}
// 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) ShipGroupJoinFleet(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) ShipGroupMaxIndex(ri int) uint {
var max uint = 0
for i := range c.g.ShipGroups {
if r := c.ShipGroupOwnerRaceIndex(i); r == ri && c.ShipGroup(i).Index > max {
max = c.ShipGroup(i).Index
}
}
return max
}
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) ShipGroupNumber(i int, n uint) {
c.validateShipGroupIndex(i)
c.g.ShipGroups[i].Number = n
// FIXME: cargo load must be decreased proportionally
}
func (c *Cache) DeleteShipGroup(i int) {
c.validateShipGroupIndex(i)
c.unsafeDeleteShipGroup(i)
}
func (c *Cache) DeleteKilledShipGroups() {
for i := len(c.g.ShipGroups) - 1; i >= 0; i-- {
if c.g.ShipGroups[i].Number == 0 {
c.unsafeDeleteShipGroup(i)
}
}
// TODO: delete empty fleets
}
func (c *Controller) JoinEqualGroups(raceName string) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
c.Cache.JoinEqualGroups(ri)
return nil
}
func (c *Cache) TurnMergeEqualShipGroups() {
for i := range c.g.Race {
c.JoinEqualGroups(i)
}
}
func (c *Cache) JoinEqualGroups(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].Index = maxUint(raceGroups[i].Index, raceGroups[j].Index)
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 _, idx := range toDelete {
c.unsafeDeleteShipGroup(idx)
}
for i := range raceGroups {
c.appendShipGroup(ri, &raceGroups[i])
}
}
func (c *Controller) BreakGroup(raceName string, groupIndex, quantity uint) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
return c.Cache.BreakGroup(ri, groupIndex, quantity)
}
func (c *Controller) DisassembleGroup(raceName string, groupIndex, quantity uint) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
return c.Cache.DisassembleGroup(ri, groupIndex, quantity)
}
func (c *Cache) DisassembleGroup(ri int, groupIndex, quantity uint) error {
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
if !ok {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if c.ShipGroup(sgi).State() != game.StateInOrbit {
return e.NewShipsBusyError()
}
if c.ShipGroup(sgi).Number < quantity {
return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity)
}
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 quantity > 0 && quantity < c.ShipGroup(sgi).Number {
// make new group for disassembly
nsgi, err := c.breakGroupSafe(ri, groupIndex, quantity)
if err != nil {
return err
}
sgi = nsgi
}
if c.ShipGroup(sgi).CargoType != nil {
ct := *c.ShipGroup(sgi).CargoType
load := c.ShipGroup(sgi).Load.F()
switch ct {
case game.CargoColonist:
if p.Owner == 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 *Controller) LoadCargo(raceName string, groupIndex uint, cargoType string, ships uint, quantity float64) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
ct, ok := game.CargoTypeSet[cargoType]
if !ok {
return e.NewCargoTypeInvalidError(cargoType)
}
return c.Cache.LoadCargo(ri, groupIndex, ct, ships, quantity)
}
// Корабль может нести только один тип груза одновременно.
// Возможные типы груза - это колонисты, сырье и промышленность.
// Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется.
func (c *Cache) LoadCargo(ri int, groupIndex uint, ct game.CargoType, ships uint, quantity float64) error {
if ships == 0 && quantity > 0 {
return e.NewCargoQuantityWithoutGroupBreakError()
}
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
if !ok {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if c.ShipGroup(sgi).State() != game.StateInOrbit {
return e.NewShipsBusyError()
}
p, ok := c.Planet(c.ShipGroup(sgi).Destination)
if !ok {
return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
}
if p.Owner != uuid.Nil && p.Owner != 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)
}
if ships > 0 && ships < c.ShipGroup(sgi).Number {
nsgi, err := c.breakGroupSafe(ri, groupIndex, ships)
if err != nil {
return err
}
sgi = nsgi
}
capacity := c.ShipGroup(sgi).CargoCapacity(st)
freeShipGroupCargoLoad := capacity - c.ShipGroup(sgi).Load.F()
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 *Controller) UnloadCargo(raceName string, groupIndex uint, ships uint, quantity float64) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
return c.Cache.UnloadCargo(ri, groupIndex, ships, quantity)
}
// Промышленность и Сырье могут быть выгружены на любой планете.
// Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты.
func (c *Cache) UnloadCargo(ri int, groupIndex uint, ships uint, quantity float64) error {
c.validateRaceIndex(ri)
if ships == 0 && quantity > 0 {
return e.NewCargoQuantityWithoutGroupBreakError()
}
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
if !ok {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if c.ShipGroup(sgi).State() != game.StateInOrbit {
return e.NewShipsBusyError()
}
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.Owner != uuid.Nil && p.Owner != c.g.Race[ri].ID {
return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct)
}
if ships > 0 && ships < c.ShipGroup(sgi).Number {
nsgi, err := c.breakGroupSafe(ri, groupIndex, ships)
if err != nil {
return err
}
sgi = nsgi
}
toBeUnloaded := quantity
if quantity == 0 {
toBeUnloaded = c.ShipGroup(sgi).Load.F()
}
if toBeUnloaded > c.ShipGroup(sgi).Load.F() {
return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load)
}
c.unsafeUnloadCargo(sgi, toBeUnloaded)
return nil
}
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.Owner == uuid.Nil {
p.Owner = 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) // TODO: apply rounding for Load property?
if c.ShipGroup(sgi).Load == 0 {
c.ShipGroup(sgi).CargoType = nil
}
p.UnpackColonists()
p.UnpackCapital()
}
func (c *Controller) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
riAccept, err := c.Cache.raceIndex(raceAcceptor)
if err != nil {
return err
}
return c.Cache.GiveawayGroup(ri, riAccept, groupIndex, quantity)
}
func (c *Cache) GiveawayGroup(ri, riAccept int, groupIndex, quantity uint) (err error) {
if ri == riAccept {
return e.NewSameRaceError(c.g.Race[riAccept].Name)
}
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
if !ok {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if c.ShipGroup(sgi).Number < quantity {
return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity)
}
st := c.ShipGroupShipClass(sgi)
var stAcc int
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]) {
return e.NewGiveawayGroupShipsTypeNotEqualError("race %q, ship type %q", c.g.Race[riAccept].Name, c.g.Race[riAccept].ShipTypes[stAcc].Name)
}
if stAcc < 0 {
err = c.CreateShipType(riAccept,
st.Name,
st.Drive,
int(st.Armament),
st.Weapons,
st.Shields,
st.Cargo)
if err != nil {
return err
}
stAcc = len(c.g.Race[riAccept].ShipTypes) - 1
}
sg := *(c.ShipGroup(sgi))
sg.TypeID = c.g.Race[riAccept].ShipTypes[stAcc].ID
sg.Number = uint(quantity)
sg.Tech = maps.Clone(sg.Tech)
c.appendShipGroup(riAccept, &sg)
if quantity == 0 || quantity == c.ShipGroup(sgi).Number {
c.unsafeDeleteShipGroup(sgi)
} else {
c.ShipGroup(sgi).Number -= quantity
}
return nil
}
func (c *Cache) BreakGroup(ri int, groupIndex, quantity uint) error {
c.validateRaceIndex(ri)
sgi := -1
for i := range c.ShipGroupsIndex() {
if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).Index == groupIndex {
sgi = i
break
}
}
if sgi < 0 {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if c.ShipGroup(sgi).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 {
c.ShipGroupJoinFleet(sgi, nil)
} else {
if _, err := c.breakGroupSafe(ri, groupIndex, quantity); err != nil {
return err
}
}
return nil
}
func (c *Cache) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int, error) {
c.validateRaceIndex(ri)
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
if !ok {
return -1, e.NewEntityNotExistsError("group #%d", groupIndex)
}
if c.ShipGroup(sgi).Number < newGroupShips {
return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", c.ShipGroup(sgi).Index, c.ShipGroup(sgi).Number, newGroupShips)
}
return c.breakGroupUnsafe(ri, sgi, newGroupShips), nil
}
func (c *Cache) breakGroupUnsafe(ri, sgi int, newGroupShips uint) int {
newGroup := *c.ShipGroup(sgi)
if c.ShipGroup(sgi).CargoType != nil {
newGroup.Load = game.F(c.ShipGroup(sgi).Load.F() / 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) appendShipGroup(ri int, sg *game.ShipGroup) int {
c.validateRaceIndex(ri)
sg.Index = c.ShipGroupMaxIndex(ri) + 1
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
}
func (c *Cache) raceShipGroupIndex(ri int, index uint) (int, bool) {
c.validateRaceIndex(ri)
for i := range c.ShipGroupsIndex() {
if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).Index == index {
return i, true
}
}
return -1, false
}
func (c *Cache) listShipGroups(ri int) iter.Seq[*game.ShipGroup] {
c.validateRaceIndex(ri)
return func(yield func(*game.ShipGroup) bool) {
for i := range c.g.ShipGroups {
if ri == c.ShipGroupOwnerRaceIndex(i) {
if !yield(&c.g.ShipGroups[i]) {
return
}
}
}
}
}
func (c *Cache) shipGroupsInUpgrade(planetNumber uint) iter.Seq[*game.ShipGroup] {
return func(yield func(*game.ShipGroup) bool) {
for sg := range c.g.ShipGroups {
if c.g.ShipGroups[sg].Destination == planetNumber && c.g.ShipGroups[sg].State() == game.StateUpgrade {
if !yield(&c.g.ShipGroups[sg]) {
break
}
}
}
}
}
func (c *Cache) unsafeDeleteShipGroup(i int) {
c.validateShipGroupIndex(i)
c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+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)))
}
}