b4abf90ec5
Per the documented turn order (game/rules.txt "Последовательность действий"), no ship should dodge the pre-departure battle by slipping into hyperspace. MakeTurn now runs merge -> battle -> load+launch routed groups -> fly -> merge -> battle, so: - ships ordered to depart (Launched) and ships being upgraded now take part in the pre-departure battle at their planet (CollectPlanetGroups / FilterBattleGroups); only survivors then enter hyperspace; - routed transports are loaded and launched AFTER that battle, so they fight empty and cannot escape it. A just-launched group has no stored hyperspace position, so moveShipGroup starts its first leg from the origin planet; the previous code read the nil launch coordinate and would panic. Because upgrading groups can now lose ships in the battle, the pending upgrade cost is recomputed from the group's current ship count instead of the value stored when the order was validated. Rules: reordered "Последовательность действий" and rewrote the combat note that ordered/routed ships skip the battle. Tests: launched-group move from origin, launched/upgrade groups taking part in battle, upgrade cost tracking ship losses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
291 lines
8.3 KiB
Go
291 lines
8.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"iter"
|
|
"slices"
|
|
|
|
"galaxy/calc"
|
|
"galaxy/util"
|
|
|
|
e "galaxy/error"
|
|
|
|
"galaxy/game/internal/model/game"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func (c *Cache) PlanetRename(ri int, number int, name string) error {
|
|
n, ok := util.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.OwnedBy(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 *Cache) PlanetProduce(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.OwnedBy(c.g.Race[ri].ID) {
|
|
return e.NewEntityNotOwnedError("planet #%d", number)
|
|
}
|
|
var subjectID *uuid.UUID
|
|
if prod == game.ResearchScience || prod == game.ProductionShip {
|
|
if _, ok := util.ValidateTypeName(subj); !ok {
|
|
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) {
|
|
p.ReleaseMaterial(c.MustShipType(ri, *p.Production.SubjectID).EmptyMass())
|
|
} else if prod == game.ProductionShip {
|
|
// new ship class to produce; otherwise we must have been returned from the func earlier
|
|
p.Production.Progress = new(game.Float)
|
|
p.Production.ProdUsed = new(game.Float)
|
|
}
|
|
|
|
if prod != game.ProductionShip {
|
|
p.Production.Progress = nil
|
|
p.Production.ProdUsed = nil
|
|
}
|
|
|
|
p.Production.Type = prod
|
|
p.Production.SubjectID = subjectID
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) PlanetProductionDisplayName(pn uint) string {
|
|
p := c.MustPlanet(pn)
|
|
if !p.Owned() {
|
|
return "-"
|
|
}
|
|
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 += c.upgradeCostNow(sg)
|
|
}
|
|
return p.ProductionCapacity() - busyResources
|
|
}
|
|
|
|
func (c *Cache) TurnPlanetProductions() {
|
|
for sgi := range c.ShipGroupsIndex() {
|
|
sg := c.ShipGroup(sgi)
|
|
// cancel upgrade for groups on wiped planets
|
|
if sg.State() == game.StateUpgrade && !c.MustPlanet(sg.Destination).Owned() {
|
|
sg.StateUpgrade = nil
|
|
}
|
|
}
|
|
|
|
for pn := range c.listProducingPlanets() {
|
|
p := c.MustPlanet(pn)
|
|
ri := c.RaceIndex(*p.Owner)
|
|
r := &c.g.Race[ri]
|
|
|
|
// upgrade groups and return to in_orbit state
|
|
productionAvailable := c.PlanetProductionCapacity(pn)
|
|
for sg := range c.shipGroupsInUpgrade(p.Number) {
|
|
cost := c.upgradeCostNow(sg)
|
|
if productionAvailable >= cost {
|
|
for i := range sg.StateUpgrade.UpgradeTech {
|
|
sg.Tech = sg.Tech.Set(sg.StateUpgrade.UpgradeTech[i].Tech, util.Fixed3(sg.StateUpgrade.UpgradeTech[i].Level.F()))
|
|
}
|
|
productionAvailable -= cost
|
|
}
|
|
sg.StateUpgrade = nil
|
|
}
|
|
|
|
switch pt := p.Production.Type; pt {
|
|
case game.ProductionShip:
|
|
st := c.MustShipType(ri, *p.Production.SubjectID)
|
|
if ships := ProduceShip(p, productionAvailable, st.EmptyMass()); ships > 0 {
|
|
c.unsafeCreateShips(ri, st.ID, p.Number, ships)
|
|
}
|
|
case game.ResearchScience:
|
|
sc := c.mustScience(ri, *p.Production.SubjectID)
|
|
ResearchTech(r, productionAvailable, sc.Drive.F(), sc.Weapons.F(), sc.Shields.F(), sc.Cargo.F())
|
|
case game.ResearchDrive:
|
|
ResearchTech(r, productionAvailable, 1., 0, 0, 0)
|
|
case game.ResearchWeapons:
|
|
ResearchTech(r, productionAvailable, 0, 1., 0, 0)
|
|
case game.ResearchShields:
|
|
ResearchTech(r, productionAvailable, 0, 0, 1., 0)
|
|
case game.ResearchCargo:
|
|
ResearchTech(r, productionAvailable, 0, 0, 0, 1.)
|
|
case game.ProductionMaterial:
|
|
p.ProduceMaterial(productionAvailable)
|
|
case game.ProductionCapital:
|
|
p.ProduceIndustry(productionAvailable)
|
|
default:
|
|
panic(fmt.Sprintf("unprocessed production type: '%v' for planet: #%d owner=%v", pt, pn, p.Owner))
|
|
}
|
|
|
|
// 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].Owned() || 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)
|
|
}
|
|
|
|
// ProduceShip returns number of ships with shipMass planet p can produce in one turn
|
|
func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
|
|
if productionAvailable <= 0 {
|
|
return 0
|
|
}
|
|
ships, materialLeft, productionUsed, progress := calc.ProduceShipsInTurn(
|
|
productionAvailable,
|
|
float64(p.Material),
|
|
float64(p.Resources),
|
|
shipMass,
|
|
)
|
|
p.Mat(materialLeft)
|
|
pval := game.F(progress)
|
|
if p.Production.Progress != nil {
|
|
pval += *p.Production.Progress
|
|
}
|
|
p.Production.Progress = &pval
|
|
used := game.F(productionUsed)
|
|
p.Production.ProdUsed = &used
|
|
return ships
|
|
}
|