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>
238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
package controller
|
|
|
|
import (
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
|
|
"galaxy/calc"
|
|
e "galaxy/error"
|
|
|
|
"galaxy/game/internal/model/game"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func (c *Cache) shipGroupUpgrade(ri int, groupID uuid.UUID, techInput string, limitLevel float64) error {
|
|
c.validateRaceIndex(ri)
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group %s", groupID)
|
|
}
|
|
st := c.ShipGroupShipClass(sgi)
|
|
sg := c.ShipGroup(sgi)
|
|
|
|
if state := sg.State(); state != game.StateInOrbit {
|
|
return e.NewShipsBusyError("state: %s", state)
|
|
}
|
|
|
|
p := c.MustPlanet(sg.Destination)
|
|
if p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
|
|
return e.NewEntityNotOwnedError("planet #%d for upgrade group %s", p.Number, groupID)
|
|
}
|
|
|
|
upgradeValidTech := map[string]game.Tech{
|
|
strings.ToLower(game.TechDrive.String()): game.TechDrive,
|
|
strings.ToLower(game.TechWeapons.String()): game.TechWeapons,
|
|
strings.ToLower(game.TechShields.String()): game.TechShields,
|
|
strings.ToLower(game.TechCargo.String()): game.TechCargo,
|
|
strings.ToLower(game.TechAll.String()): game.TechAll,
|
|
}
|
|
|
|
techRequest, ok := upgradeValidTech[strings.ToLower(techInput)]
|
|
if !ok {
|
|
return e.NewTechUnknownError(techInput)
|
|
}
|
|
|
|
var blockMasses map[game.Tech]float64 = map[game.Tech]float64{
|
|
game.TechDrive: st.DriveBlockMass(),
|
|
game.TechWeapons: st.WeaponsBlockMass(),
|
|
game.TechShields: st.ShieldsBlockMass(),
|
|
game.TechCargo: st.CargoBlockMass(),
|
|
}
|
|
|
|
switch {
|
|
case techRequest != game.TechAll && blockMasses[techRequest] == 0:
|
|
return e.NewUpgradeShipTechNotUsedError()
|
|
case techRequest == game.TechAll && limitLevel != 0:
|
|
return e.NewUpgradeParameterNotAllowedError("tech=%s max_level=%f", techRequest.String(), limitLevel)
|
|
}
|
|
|
|
targetLevel := make(map[game.Tech]float64)
|
|
var sumLevels float64
|
|
for _, tech := range []game.Tech{game.TechDrive, game.TechWeapons, game.TechShields, game.TechCargo} {
|
|
if techRequest == game.TechAll || tech == techRequest {
|
|
if c.g.Race[ri].TechLevel(tech) < limitLevel {
|
|
return e.NewUpgradeTechLevelInsufficientError("%s=%.03f < %.03f", tech.String(), c.g.Race[ri].TechLevel(tech), limitLevel)
|
|
}
|
|
targetLevel[tech] = FutureUpgradeLevel(c.g.Race[ri].TechLevel(tech), sg.TechLevel(tech).F(), limitLevel)
|
|
} else {
|
|
targetLevel[tech] = CurrentUpgradingLevel(sg, tech)
|
|
}
|
|
sumLevels += targetLevel[tech]
|
|
}
|
|
|
|
productionCapacity := c.PlanetProductionCapacity(p.Number)
|
|
uc := GroupUpgradeCost(sg, *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo])
|
|
costForShip := uc.UpgradeCost(1)
|
|
if costForShip == 0 {
|
|
return e.NewUpgradeShipsAlreadyUpToDateError("%#v", targetLevel)
|
|
}
|
|
|
|
shipsToUpgrade := sg.Number
|
|
maxUpgradableShips := uc.UpgradeMaxShips(productionCapacity)
|
|
|
|
/*
|
|
1. считаем стоимость модернизации одного корабля
|
|
2. считаем сколько кораблей можно модернизировать
|
|
3. если не хватает даже на 1 корабль, ограничиваемся одним кораблём и пересчитываем коэффициент пропорционально массе блоков
|
|
4. иначе, считаем истинное количество кораблей с учётом ограничения maxShips
|
|
*/
|
|
blockMassSum := st.EmptyMass()
|
|
|
|
coef := productionCapacity / costForShip
|
|
if maxUpgradableShips == 0 {
|
|
if limitLevel > 0 {
|
|
return e.NewUpgradeInsufficientResourcesError("ship cost=%.03f L=%.03f", costForShip, productionCapacity)
|
|
}
|
|
sumLevels = sumLevels * coef
|
|
for tech := range targetLevel {
|
|
if blockMasses[tech] > 0 {
|
|
proportional := sumLevels * (blockMasses[tech] / blockMassSum)
|
|
targetLevel[tech] = proportional
|
|
}
|
|
}
|
|
maxUpgradableShips = 1
|
|
} else if maxUpgradableShips > shipsToUpgrade {
|
|
maxUpgradableShips = shipsToUpgrade
|
|
}
|
|
|
|
// sanity check
|
|
uc = GroupUpgradeCost(sg, *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo])
|
|
costForGroup := uc.UpgradeCost(maxUpgradableShips)
|
|
if costForGroup > productionCapacity {
|
|
e.NewGameStateError("cost recalculation: coef=%f cost(%d)=%f L=%f", coef, maxUpgradableShips, costForGroup, productionCapacity)
|
|
}
|
|
|
|
// break group if needed
|
|
if maxUpgradableShips < sg.Number {
|
|
nsgi, err := c.breakGroup(ri, groupID, maxUpgradableShips)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sgi = nsgi
|
|
}
|
|
|
|
// finally, fill group upgrade prefs
|
|
for tech := range targetLevel {
|
|
if targetLevel[tech] > 0 {
|
|
c.UpgradeShipGroup(sgi, tech, targetLevel[tech])
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) UpgradeShipGroup(sgi int, tech game.Tech, v float64) {
|
|
sg := *(c.ShipGroup(sgi))
|
|
st := c.ShipGroupShipClass(sgi)
|
|
c.g.ShipGroups[sgi] = UpgradeGroupPreference(sg, *st, tech, v)
|
|
}
|
|
|
|
// helpers
|
|
|
|
type UpgradeCalc struct {
|
|
Cost map[game.Tech]float64
|
|
}
|
|
|
|
func (uc UpgradeCalc) UpgradeCost(ships uint) float64 {
|
|
var sum float64
|
|
for _, v := range uc.Cost {
|
|
sum += v
|
|
}
|
|
return sum * float64(ships)
|
|
}
|
|
|
|
func (uc UpgradeCalc) UpgradeMaxShips(resources float64) uint {
|
|
return uint(math.Floor(resources / uc.UpgradeCost(1)))
|
|
}
|
|
|
|
func GroupUpgradeCost(sg *game.ShipGroup, st game.ShipType, drive, weapons, shields, cargo float64) UpgradeCalc {
|
|
uc := &UpgradeCalc{Cost: make(map[game.Tech]float64)}
|
|
if drive > 0 {
|
|
uc.Cost[game.TechDrive] = calc.BlockUpgradeCost(st.DriveBlockMass(), sg.TechLevel(game.TechDrive).F(), drive)
|
|
}
|
|
if weapons > 0 {
|
|
uc.Cost[game.TechWeapons] = calc.BlockUpgradeCost(st.WeaponsBlockMass(), sg.TechLevel(game.TechWeapons).F(), weapons)
|
|
}
|
|
if shields > 0 {
|
|
uc.Cost[game.TechShields] = calc.BlockUpgradeCost(st.ShieldsBlockMass(), sg.TechLevel(game.TechShields).F(), shields)
|
|
}
|
|
if cargo > 0 {
|
|
uc.Cost[game.TechCargo] = calc.BlockUpgradeCost(st.CargoBlockMass(), sg.TechLevel(game.TechCargo).F(), cargo)
|
|
}
|
|
return *uc
|
|
}
|
|
|
|
func CurrentUpgradingLevel(sg *game.ShipGroup, tech game.Tech) float64 {
|
|
if sg.StateUpgrade == nil {
|
|
return 0
|
|
}
|
|
ti := slices.IndexFunc(sg.StateUpgrade.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech })
|
|
if ti >= 0 {
|
|
return sg.StateUpgrade.UpgradeTech[ti].Level.F()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func FutureUpgradeLevel(raceLevel, groupLevel, limit float64) float64 {
|
|
target := limit
|
|
if target == 0 || target > raceLevel {
|
|
target = raceLevel
|
|
}
|
|
if groupLevel == target {
|
|
return 0
|
|
}
|
|
return target
|
|
}
|
|
|
|
// upgradeCostNow returns the production cost to apply group sg's pending
|
|
// upgrade to its CURRENT ship count. The cost stored on StateUpgrade is fixed
|
|
// when the order is validated; if the group has since lost ships (for example
|
|
// in the pre-departure battle, now that upgrading groups take part in it), the
|
|
// stored total is stale, so the cost is recomputed from the stored target
|
|
// levels, the group's current tech, and its current ship count.
|
|
func (c *Cache) upgradeCostNow(sg *game.ShipGroup) float64 {
|
|
if sg.StateUpgrade == nil {
|
|
return 0
|
|
}
|
|
st := c.MustShipType(c.RaceIndex(sg.OwnerID), sg.TypeID)
|
|
var perShip float64
|
|
for _, pref := range sg.StateUpgrade.UpgradeTech {
|
|
perShip += calc.BlockUpgradeCost(st.BlockMass(pref.Tech), sg.TechLevel(pref.Tech).F(), pref.Level.F())
|
|
}
|
|
return perShip * float64(sg.Number)
|
|
}
|
|
|
|
func UpgradeGroupPreference(sg game.ShipGroup, st game.ShipType, tech game.Tech, v float64) game.ShipGroup {
|
|
if v <= 0 || st.BlockMass(tech) == 0 || sg.TechLevel(tech).F() >= v {
|
|
return sg
|
|
}
|
|
var su game.InUpgrade
|
|
if sg.StateUpgrade != nil {
|
|
su = *sg.StateUpgrade
|
|
} else {
|
|
su = game.InUpgrade{UpgradeTech: []game.UpgradePreference{}}
|
|
}
|
|
ti := slices.IndexFunc(su.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech })
|
|
if ti < 0 {
|
|
su.UpgradeTech = append(su.UpgradeTech, game.UpgradePreference{Tech: tech})
|
|
ti = len(su.UpgradeTech) - 1
|
|
}
|
|
su.UpgradeTech[ti].Level = game.F(v)
|
|
su.UpgradeTech[ti].Cost = game.F(calc.BlockUpgradeCost(st.BlockMass(tech), sg.TechLevel(tech).F(), v) * float64(sg.Number))
|
|
|
|
sg.StateUpgrade = &su
|
|
return sg
|
|
}
|