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

308 lines
8.8 KiB
Go

package controller
import (
"fmt"
"iter"
"slices"
"strings"
"github.com/google/uuid"
e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/model/game"
)
func (c *Controller) RenamePlanet(raceName string, planetNumber int, typeName string) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
return c.Cache.RenamePlanet(ri, planetNumber, typeName)
}
func (c *Cache) RenamePlanet(ri int, number int, name string) error {
n, ok := validateTypeName(name)
if !ok {
return e.NewEntityTypeNameValidationError("%q", n)
}
if number < 0 {
return e.NewPlanetNumberError(number)
}
p, ok := c.Planet(uint(number))
if !ok {
return e.NewEntityNotExistsError("planet #%d", number)
}
if p.Owner != c.g.Race[ri].ID {
return e.NewEntityNotOwnedError("planet #%d", number)
}
c.g.Map.Planet[c.MustPlanetIndex(p.Number)].Name = n
return nil
}
func (c *Controller) PlanetProduction(raceName string, planetNumber int, prodType, subject string) error {
ri, err := c.Cache.raceIndex(raceName)
if err != nil {
return err
}
var prod game.ProductionType
switch game.ProductionType(strings.ToUpper(prodType)) {
case game.ProductionMaterial:
prod = game.ProductionMaterial
case game.ProductionCapital:
prod = game.ProductionCapital
case game.ResearchDrive:
prod = game.ResearchDrive
case game.ResearchWeapons:
prod = game.ResearchWeapons
case game.ResearchShields:
prod = game.ResearchShields
case game.ResearchCargo:
prod = game.ResearchCargo
case game.ResearchScience:
prod = game.ResearchScience
case game.ProductionShip:
prod = game.ProductionShip
default:
return e.NewProductionInvalidError(prodType)
}
return c.Cache.PlanetProduction(ri, planetNumber, prod, subject)
}
func (c *Cache) PlanetProduction(ri int, number int, prod game.ProductionType, subj string) error {
c.validateRaceIndex(ri)
if number < 0 {
return e.NewPlanetNumberError(number)
}
p, ok := c.Planet(uint(number))
if !ok {
return e.NewEntityNotExistsError("planet #%d", number)
}
if p.Owner != c.g.Race[ri].ID {
return e.NewEntityNotOwnedError("planet #%d", number)
}
var subjectID *uuid.UUID
if (prod == game.ResearchScience || prod == game.ProductionShip) && subj == "" {
return e.NewEntityTypeNameValidationError("%s=%q", prod, subj)
}
if prod == game.ResearchScience {
i := slices.IndexFunc(c.g.Race[ri].Sciences, func(s game.Science) bool { return s.Name == subj })
if i < 0 {
return e.NewEntityNotExistsError("science %q", subj)
}
subjectID = &c.g.Race[ri].Sciences[i].ID
}
if prod == game.ProductionShip {
st, _, ok := c.ShipClass(ri, subj)
if !ok {
return e.NewEntityNotExistsError("ship type %q", subj)
}
if p.Production.Type == game.ProductionShip &&
p.Production.SubjectID != nil &&
*p.Production.SubjectID == st.ID {
// Planet already produces this ship type, keeping progress intact
return nil
}
subjectID = &st.ID
}
if p.Production.Type == game.ProductionShip && (prod != game.ProductionShip || *subjectID != *p.Production.SubjectID) {
mat, _ := c.MustShipType(ri, *p.Production.SubjectID).ProductionCost()
p.Mat(p.Material.F() + mat*(*p.Production.Progress))
*p.Production.Progress = 0.
} else if prod == game.ProductionShip {
// new ship class to produce; otherwise we must have been returned from the func earlier
var progress float64 = 0.
p.Production.Progress = &progress
}
if prod != game.ProductionShip {
p.Production.Progress = nil
}
p.Production.Type = prod
p.Production.SubjectID = subjectID
return nil
}
// TODO: test
func (c *Cache) PlanetProductionDisplayName(pn uint) string {
p := c.MustPlanet(pn)
ri := c.RaceIndex(p.Owner)
switch pt := p.Production.Type; pt {
case game.ResearchDrive:
return "Drive"
case game.ResearchWeapons:
return "Weapons"
case game.ResearchShields:
return "Shields"
case game.ResearchCargo:
return "Cargo"
case game.ProductionMaterial:
return "Material"
case game.ProductionCapital:
return "Capital"
case game.ProductionShip:
return c.MustShipType(ri, *p.Production.SubjectID).Name
case game.ResearchScience:
i := slices.IndexFunc(c.g.Race[ri].Sciences, func(sc game.Science) bool { return sc.ID == *p.Production.SubjectID })
if i < 0 {
panic("researching science not found")
}
return c.g.Race[ri].Sciences[i].Name
default:
return string(pt)
}
}
func (c *Cache) Planet(planetNumber uint) (*game.Planet, bool) {
if c.cachePlanetByPlanetNumber == nil {
c.cachePlanetByPlanetNumber = make(map[uint]*game.Planet)
for p := range c.g.Map.Planet {
c.cachePlanetByPlanetNumber[c.g.Map.Planet[p].Number] = &c.g.Map.Planet[p]
}
}
if v, ok := c.cachePlanetByPlanetNumber[planetNumber]; ok {
return v, true
} else {
return nil, false
}
}
func (c *Cache) MustPlanet(pn uint) *game.Planet {
if v, ok := c.Planet(pn); ok {
return v
} else {
panic(fmt.Sprintf("planet not found by number=%d", pn))
}
}
func (c *Cache) MustPlanetIndex(pn uint) int {
if idx := slices.IndexFunc(c.g.Map.Planet, func(p game.Planet) bool { return p.Number == pn }); idx < 0 {
panic(fmt.Sprintf("planet not found by number=%d", pn))
} else {
return idx
}
}
// Свободный "Производственный Потенциал" (L)
// промышленность * 0.75 + население * 0.25
// за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
func (c *Cache) PlanetProductionCapacity(planetNumber uint) float64 {
p := c.MustPlanet(planetNumber)
var busyResources float64
for sg := range c.shipGroupsInUpgrade(p.Number) {
busyResources += sg.StateUpgrade.Cost()
}
return p.ProductionCapacity() - busyResources
}
func (c *Cache) TurnPlanetProductions() {
for pn := range c.listProducingPlanets() {
p := c.MustPlanet(pn)
ri := c.RaceIndex(p.Owner)
r := &c.g.Race[ri]
switch pt := p.Production.Type; pt {
case game.ProductionShip:
st := c.MustShipType(ri, *p.Production.SubjectID)
if ships := ProduceShip(p, st.EmptyMass()); ships > 0 {
c.createShipsUnsafe(ri, st.ID, p.Number, ships)
}
case game.ResearchScience:
sc := c.mustScience(ri, *p.Production.SubjectID)
ResearchTech(r, p.ProductionCapacity(), sc.Drive, sc.Weapons, sc.Shields, sc.Cargo)
case game.ResearchDrive:
ResearchTech(r, p.ProductionCapacity(), 1., 0, 0, 0)
case game.ResearchWeapons:
ResearchTech(r, p.ProductionCapacity(), 0, 1., 0, 0)
case game.ResearchShields:
ResearchTech(r, p.ProductionCapacity(), 0, 0, 1., 0)
case game.ResearchCargo:
ResearchTech(r, p.ProductionCapacity(), 0, 0, 0, 1.)
case game.ProductionMaterial:
p.ProduceMaterial()
case game.ProductionCapital:
p.ProduceIndustry()
default:
panic(fmt.Sprintf("unprocessed production type: %v", pt))
}
// last step: increase population / colonists
p.ProducePopulation()
}
c.TurnMergeEqualShipGroups()
}
// listProducingPlanets iterates over all inhabited planet numbers with defined production type.
// Planets producing ships guaranteed to be iterated first for correct turn actions order.
func (c *Cache) listProducingPlanets() iter.Seq[uint] {
ordered := make([]int, 0)
for i := range c.g.Map.Planet {
if c.g.Map.Planet[i].Owner == uuid.Nil || c.g.Map.Planet[i].Production.Type == game.ProductionNone {
continue
}
ordered = append(ordered, i)
}
slices.SortFunc(ordered, func(l, r int) int {
if c.g.Map.Planet[l].Production.Type == game.ProductionShip && c.g.Map.Planet[r].Production.Type != game.ProductionShip {
return -1
}
if c.g.Map.Planet[l].Production.Type != game.ProductionShip && c.g.Map.Planet[r].Production.Type == game.ProductionShip {
return 1
}
return 0
})
return func(yield func(uint) bool) {
for _, i := range ordered {
if !yield(c.g.Map.Planet[i].Number) {
return
}
}
}
}
// Internal funcs
func (c *Cache) putPopulation(pn uint, v float64) {
c.MustPlanet(pn).Pop(v)
}
func (c *Cache) putColonists(pn uint, v float64) {
c.MustPlanet(pn).Col(v)
}
func (c *Cache) putMaterial(pn uint, v float64) {
c.MustPlanet(pn).Mat(v)
}
func ProduceShip(p *game.Planet, shipMass float64) uint {
productionAvailable := p.ProductionCapacity()
if productionAvailable <= 0 {
return 0
}
CAP_perShip := shipMass / p.Resources.F()
productionForMass := shipMass * 10.
ships := uint(0)
flZero := 0.
p.Production.Progress = &flZero
for productionAvailable > 0 {
var productionExtraCAP float64
if CAP_deficit := p.Capital.F() - CAP_perShip; CAP_deficit < 0 {
productionExtraCAP = -CAP_deficit
}
productionCost := productionExtraCAP + productionForMass
if productionAvailable >= productionCost {
productionAvailable -= productionCost
p.Cap(p.Capital.F() - (CAP_perShip - productionExtraCAP))
ships++
} else {
progress := productionAvailable / productionCost
productionAvailable -= productionCost * progress
p.Production.Progress = &progress
break
}
}
return ships
}