Files
galaxy-game/game/internal/controller/ship_group_upgrade.go
T
Ilia Denisov 3626998a33 ui/phase-20: ship-group inspector actions
Eight ship-group operations land on the inspector behind a single
inline-form panel: split, send, load, unload, modernize, dismantle,
transfer, join fleet. Each action either appends a typed command to
the local order draft or surfaces a tooltip explaining the
disabled state. Partial-ship operations emit an implicit
breakShipGroup command before the targeted action so the engine
sees a clean (Break, Action) pair on the wire.

`pkg/calc.BlockUpgradeCost` migrates from
`game/internal/controller/ship_group_upgrade.go` so the calc
bridge can wrap a pure pkg/calc formula; the controller now
imports it. The bridge surfaces the function as
`core.blockUpgradeCost`, which the inspector calls once per ship
block to render the modernize cost preview.

`GameReport.otherRaces` is decoded from the report's player block
(non-extinct, ≠ self) and feeds the transfer-to-race picker. The
planet inspector's stationed-ship rows become clickable for own
groups so the actions panel is reachable from the standard click
flow (the renderer continues to hide on-planet groups).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:27:55 +02:00

220 lines
7.1 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
}
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
}