package game import ( "math" "slices" "github.com/google/uuid" e "github.com/iliadenisov/galaxy/pkg/error" "github.com/iliadenisov/galaxy/pkg/number" ) type ShipTypeReport struct { Name string `json:"name"` Drive float64 `json:"drive"` Armament uint `json:"armament"` Weapons float64 `json:"weapons"` Shields float64 `json:"shields"` Cargo float64 `json:"cargo"` } type ShipType struct { ID uuid.UUID `json:"id"` ShipTypeReport } type ShipTypeReportForeign struct { RaceName string ShipTypeReport } type ShipGroup struct { TypeID uuid.UUID `json:"id"` Type ShipType `json:"-"` // TODO: fill upon load from store Number uint `json:"number"` State string `json:"state"` // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade Load float64 `json:"load"` // Cargo loaded - "Масса груза" Drive float64 `json:"drive"` Weapons float64 `json:"weapons"` Shields float64 `json:"shields"` Cargo float64 `json:"cargo"` } type Fleet struct { ShipGroups []ShipGroup `json:"group"` } func (st ShipType) Equal(o ShipType) bool { return st.Drive == o.Drive && st.Weapons == o.Weapons && st.Armament == o.Armament && st.Shields == o.Shields && st.Cargo == o.Cargo } func (st ShipType) EmptyMass() float64 { shipMass := st.Drive + st.Shields + st.Cargo + st.WeaponsMass() return shipMass } func (st ShipType) WeaponsMass() float64 { if st.Armament == 0 || st.Weapons == 0 { return 0 } return float64(st.Armament+1) * (st.Weapons / 2) } // ProductionCost returns Material (MAT) and Population (POP) to produce this [ShipType] func (st ShipType) ProductionCost() (mat float64, pop float64) { mat = st.EmptyMass() pop = mat * 10 return } // Грузоподъёмность func (sg ShipGroup) CargoCapacity() float64 { return sg.Drive * (sg.Type.Cargo + (sg.Type.Cargo*sg.Type.Cargo)/20) } // "Масса перевозимого груза" func (sg ShipGroup) CarryingMass() float64 { return sg.Load / sg.Cargo } func (sg ShipGroup) FullMass() float64 { return sg.Type.EmptyMass() + sg.CarryingMass() } // "Эффективность двигателя" // равна мощности Двигателей умноженной на текущий технологический уровень блока Двигателей func (sg ShipGroup) DriveEffective() float64 { return sg.Type.Drive * sg.Drive } // TODO: test this func (sg ShipGroup) Speed() float64 { return sg.DriveEffective() * 20 / sg.FullMass() } func (sg ShipGroup) UpgradeDriveCost(drive float64) float64 { return (1 - sg.Drive/drive) * 10 * sg.Type.Drive } // TODO: test on other values func (sg ShipGroup) UpgradeWeaponsCost(weapons float64) float64 { return (1 - sg.Weapons/weapons) * 10 * sg.Type.WeaponsMass() } func (sg ShipGroup) UpgradeShieldsCost(shields float64) float64 { return (1 - sg.Shields/shields) * 10 * sg.Type.Shields } func (sg ShipGroup) UpgradeCargoCost(cargo float64) float64 { return (1 - sg.Cargo/cargo) * 10 * sg.Type.Cargo } // Мощность бомбардировки // TODO: maybe rounding must be done only for display? func (sg ShipGroup) BombingPower() float64 { // return math.Sqrt(sg.Type.Weapons * sg.Weapons) result := (math.Sqrt(sg.Type.Weapons*sg.Weapons)/10. + 1.) * sg.Type.Weapons * sg.Weapons * float64(sg.Type.Armament) * float64(sg.Number) return number.Fixed3(result) } // TODO: test this func (fl Fleet) Speed() float64 { result := math.MaxFloat64 for _, sg := range fl.ShipGroups { if sg.Speed() < result { result = sg.Speed() } } return result } func (g Game) ShipTypes(raceName string) ([]ShipType, error) { ri, err := g.raceIndex(raceName) if err != nil { return nil, err } return g.shipTypesInternal(ri), nil } func (g Game) shipTypesInternal(ri int) []ShipType { return g.Race[ri].ShipTypes } func (g Game) DeleteShipType(raceName, typeName string) error { ri, err := g.raceIndex(raceName) if err != nil { return err } return g.deleteShipTypeInternal(ri, typeName) } func (g Game) deleteShipTypeInternal(ri int, name string) error { st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }) if st < 0 { return e.NewEntityNotExistsError("ship type %w", name) } if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Production.Production == ProductionShip && p.Production.SubjectID != nil && g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID }); pl >= 0 { return e.NewDeleteShipTypePlanetProductionError(g.Map.Planet[pl].Name) } g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...) return nil } func (g Game) CreateShipType(raceName, typeName string, d, w, s, c float64, a int) error { ri, err := g.raceIndex(raceName) if err != nil { return err } return g.createShipTypeInternal(ri, typeName, d, w, s, c, a) } func (g Game) createShipTypeInternal(ri int, name string, d, w, s, c float64, a int) error { if err := checkShipTypeValues(d, w, s, c, a); err != nil { return err } n, ok := validateTypeName(name) if !ok { return e.NewEntityTypeNameValidationError("%q", n) } if st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }); st >= 0 { return e.NewEntityTypeNameDuplicateError("ship type %w", g.Race[ri].ShipTypes[st].Name) } g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes, ShipType{ ID: uuid.New(), ShipTypeReport: ShipTypeReport{ Name: n, Drive: d, Weapons: w, Shields: s, Cargo: c, Armament: uint(a), }, }) return nil } func (g Game) MergeShipType(race, name, targetName string) error { ri, err := g.raceIndex(race) if err != nil { return err } return g.mergeShipTypeInternal(ri, name, targetName) } func (g Game) mergeShipTypeInternal(ri int, name, targetName string) error { st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }) if st < 0 { return e.NewEntityNotExistsError("source ship type %w", name) } if name == targetName { return e.NewEntityTypeNameEqualityError("ship type %q", targetName) } tt := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == targetName }) if tt < 0 { return e.NewEntityNotExistsError("target ship type %w", name) } if !g.Race[ri].ShipTypes[st].Equal(g.Race[ri].ShipTypes[tt]) { return e.NewMergeShipTypeNotEqualError() } // switch planet productions to the new type for pl := range g.Map.Planet { if g.Map.Planet[pl].Owner == g.Race[ri].ID && g.Map.Planet[pl].Production.Production == ProductionShip && g.Map.Planet[pl].Production.SubjectID != nil && *g.Map.Planet[pl].Production.SubjectID == g.Race[ri].ShipTypes[st].ID { g.Map.Planet[pl].Production.SubjectID = &g.Race[ri].ShipTypes[tt].ID } } // switch ship groups to the new type for sg := range g.Race[ri].ShipGroups { if g.Race[ri].ShipGroups[sg].TypeID == g.Race[ri].ShipTypes[st].ID { g.Race[ri].ShipGroups[sg].TypeID = g.Race[ri].ShipTypes[tt].ID } } // remove the source type g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...) return nil } func checkShipTypeValues(d, w, s, c float64, a int) error { if !checkShipTypeValueDWSC(d) { return e.NewDriveValueError(d) } if !checkShipTypeValueDWSC(w) { return e.NewWeaponsValueError(w) } if !checkShipTypeValueDWSC(s) { return e.NewShieldsValueError(s) } if !checkShipTypeValueDWSC(c) { return e.NewCargoValueError(s) } if a < 0 { return e.NewShipTypeArmamentValueError(a) } if (w == 0 && a > 0) || (a == 0 && w > 0) { return e.NewShipTypeArmamentAndWeaponsValueError("A=%d W=%.0f", a, w) } if d == 0 && w == 0 && s == 0 && c == 0 && a == 0 { return e.NewShipTypeShipTypeZeroValuesError() } return nil } func checkShipTypeValueDWSC(v float64) bool { return v == 0 || v >= 1 }