347 lines
10 KiB
Go
347 lines
10 KiB
Go
package game
|
|
|
|
import (
|
|
"iter"
|
|
"maps"
|
|
"math"
|
|
"slices"
|
|
|
|
"github.com/google/uuid"
|
|
e "github.com/iliadenisov/galaxy/internal/error"
|
|
"github.com/iliadenisov/galaxy/internal/number"
|
|
)
|
|
|
|
type CargoType string
|
|
|
|
const (
|
|
// CargoNone CargoType = "-"
|
|
CargoColonist CargoType = "COL" // Колонисты
|
|
CargoMaterial CargoType = "MAT" // Сырьё
|
|
CargoCapital CargoType = "CAP" // Промышленность
|
|
)
|
|
|
|
func (ct CargoType) Ref() *CargoType {
|
|
return &ct
|
|
}
|
|
|
|
type ShipGroup struct {
|
|
Index uint `json:"index"` // FIXME: use UUID for Group Index (ordered)
|
|
OwnerID uuid.UUID `json:"ownerId"` // Race link
|
|
TypeID uuid.UUID `json:"typeId"` // ShipType link
|
|
FleetID *uuid.UUID `json:"fleetId,omitempty"` // Fleet link
|
|
Number uint `json:"number"` // Number (quantity) ships of specific ShipType
|
|
State string `json:"state"` // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade
|
|
|
|
CargoType *CargoType `json:"loadType,omitempty"`
|
|
Load float64 `json:"load"` // Cargo loaded - "Масса груза"
|
|
|
|
Drive float64 `json:"drive"`
|
|
Weapons float64 `json:"weapons"`
|
|
Shields float64 `json:"shields"`
|
|
Cargo float64 `json:"cargo"`
|
|
|
|
// TODO: TEST: Destination, Origin, Range
|
|
Destination uint `json:"destination"`
|
|
Origin *uint `json:"origin,omitempty"`
|
|
Range *float64 `json:"range,omitempty"`
|
|
}
|
|
|
|
func (sg ShipGroup) Equal(other ShipGroup) bool {
|
|
return sg.OwnerID == other.OwnerID &&
|
|
sg.TypeID == other.TypeID &&
|
|
sg.FleetID == other.FleetID &&
|
|
sg.Drive == other.Drive &&
|
|
sg.Weapons == other.Weapons &&
|
|
sg.Shields == other.Shields &&
|
|
sg.Cargo == other.Cargo &&
|
|
sg.CargoType == other.CargoType &&
|
|
sg.Load == other.Load &&
|
|
sg.State == other.State
|
|
}
|
|
|
|
// Грузоподъёмность
|
|
func (sg ShipGroup) CargoCapacity(st *ShipType) float64 {
|
|
return sg.Cargo * (st.Cargo + (st.Cargo*st.Cargo)/20) * float64(sg.Number)
|
|
}
|
|
|
|
// Масса перевозимого груза -
|
|
// общее количество единиц груза, деленное на технологический уровень Грузоперевозок
|
|
func (sg ShipGroup) CarryingMass() float64 {
|
|
return sg.Load / sg.Cargo
|
|
}
|
|
|
|
// Полная масса -
|
|
// массу корабля самого по себе плюс масса перевозимого груза.
|
|
func (sg ShipGroup) FullMass(st *ShipType) float64 {
|
|
return st.EmptyMass() + sg.CarryingMass()
|
|
}
|
|
|
|
// Эффективность двигателя -
|
|
// равна мощности Двигателей, умноженной на технологический уровень блока Двигателей
|
|
func (sg ShipGroup) DriveEffective(st *ShipType) float64 {
|
|
return st.Drive * sg.Drive
|
|
}
|
|
|
|
// Корабли перемещаются за один ход на количество световых лет, равное
|
|
// эффективности двигателя, умноженной на 20 и деленной на "Полную массу" корабля.
|
|
func (sg ShipGroup) Speed(st *ShipType) float64 {
|
|
return sg.DriveEffective(st) * 20 / sg.FullMass(st)
|
|
}
|
|
|
|
func (sg ShipGroup) UpgradeDriveCost(st *ShipType, drive float64) float64 {
|
|
return (1 - sg.Drive/drive) * 10 * st.Drive
|
|
}
|
|
|
|
// TODO: test on other values
|
|
func (sg ShipGroup) UpgradeWeaponsCost(st *ShipType, weapons float64) float64 {
|
|
return (1 - sg.Weapons/weapons) * 10 * st.WeaponsMass()
|
|
}
|
|
|
|
func (sg ShipGroup) UpgradeShieldsCost(st *ShipType, shields float64) float64 {
|
|
return (1 - sg.Shields/shields) * 10 * st.Shields
|
|
}
|
|
|
|
func (sg ShipGroup) UpgradeCargoCost(st *ShipType, cargo float64) float64 {
|
|
return (1 - sg.Cargo/cargo) * 10 * st.Cargo
|
|
}
|
|
|
|
// Мощность бомбардировки
|
|
// TODO: maybe rounding must be done only for display?
|
|
func (sg ShipGroup) BombingPower(st *ShipType) float64 {
|
|
// return math.Sqrt(sg.Type.Weapons * sg.Weapons)
|
|
result := (math.Sqrt(st.Weapons*sg.Weapons)/10. + 1.) *
|
|
st.Weapons *
|
|
sg.Weapons *
|
|
float64(st.Armament) *
|
|
float64(sg.Number)
|
|
return number.Fixed3(result)
|
|
}
|
|
|
|
func (g *Game) JoinEqualGroups(raceName string) error {
|
|
ri, err := g.raceIndex(raceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g.joinEqualGroupsInternal(ri)
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) BreakGroup(raceName string, groupIndex, quantity uint) error {
|
|
ri, err := g.raceIndex(raceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return g.breakGroupInternal(ri, groupIndex, quantity)
|
|
}
|
|
|
|
func (g *Game) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error {
|
|
ri, err := g.raceIndex(raceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
riAccept, err := g.raceIndex(raceAcceptor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return g.giveawayGroupInternal(ri, riAccept, groupIndex, quantity)
|
|
}
|
|
|
|
func (g *Game) giveawayGroupInternal(ri, riAccept int, groupIndex, quantity uint) (err error) {
|
|
if ri == riAccept {
|
|
return e.NewInputSameRaceError(g.Race[riAccept].Name)
|
|
}
|
|
sgi := -1
|
|
for i, sg := range g.listIndexShipGroups(ri) {
|
|
if sgi < 0 && sg.Index == groupIndex {
|
|
sgi = i
|
|
}
|
|
}
|
|
if sgi < 0 {
|
|
return e.NewEntityNotExistsError("group #%d", groupIndex)
|
|
}
|
|
if g.ShipGroups[sgi].Number < quantity {
|
|
return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity)
|
|
}
|
|
|
|
var sti int
|
|
if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == g.ShipGroups[sgi].TypeID }); sti < 0 {
|
|
// hard to test, need manual game data invalidation
|
|
return e.NewGameStateError("not found: ShipType ID=%v", g.ShipGroups[sgi].TypeID)
|
|
}
|
|
|
|
var stAcc int
|
|
if stAcc = slices.IndexFunc(g.Race[riAccept].ShipTypes, func(st ShipType) bool { return st.Name == g.Race[ri].ShipTypes[sti].Name }); stAcc >= 0 &&
|
|
!g.Race[ri].ShipTypes[sti].Equal(g.Race[riAccept].ShipTypes[stAcc]) {
|
|
return e.NewGiveawayGroupShipsTypeNotEqualError("race %w, ship type %w", g.Race[riAccept].Name, g.Race[riAccept].ShipTypes[stAcc].Name)
|
|
}
|
|
if stAcc < 0 {
|
|
stAcc, err = g.createShipTypeInternal(riAccept,
|
|
g.Race[ri].ShipTypes[sti].Name,
|
|
g.Race[ri].ShipTypes[sti].Drive,
|
|
g.Race[ri].ShipTypes[sti].Weapons,
|
|
g.Race[ri].ShipTypes[sti].Shields,
|
|
g.Race[ri].ShipTypes[sti].Cargo,
|
|
int(g.Race[ri].ShipTypes[sti].Armament))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var maxIndex uint
|
|
for sg := range g.listShipGroups(riAccept) {
|
|
if sg.Index > maxIndex {
|
|
maxIndex = sg.Index
|
|
}
|
|
}
|
|
|
|
g.ShipGroups = append(g.ShipGroups, ShipGroup{
|
|
Index: maxIndex + 1,
|
|
OwnerID: g.Race[riAccept].ID,
|
|
TypeID: g.Race[riAccept].ShipTypes[stAcc].ID,
|
|
Number: uint(quantity),
|
|
State: g.ShipGroups[sgi].State,
|
|
|
|
CargoType: g.ShipGroups[sgi].CargoType,
|
|
Load: g.ShipGroups[sgi].Load,
|
|
|
|
Drive: g.ShipGroups[sgi].Drive,
|
|
Weapons: g.ShipGroups[sgi].Weapons,
|
|
Shields: g.ShipGroups[sgi].Shields,
|
|
Cargo: g.ShipGroups[sgi].Cargo,
|
|
|
|
Destination: g.ShipGroups[sgi].Destination,
|
|
Origin: g.ShipGroups[sgi].Origin,
|
|
Range: g.ShipGroups[sgi].Range,
|
|
})
|
|
|
|
if quantity == 0 || quantity == g.ShipGroups[sgi].Number {
|
|
g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...)
|
|
} else {
|
|
g.ShipGroups[sgi].Number -= quantity
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) breakGroupInternal(ri int, groupIndex, quantity uint) error {
|
|
sgi := -1
|
|
var maxIndex uint
|
|
for i, sg := range g.listIndexShipGroups(ri) {
|
|
if sgi < 0 && sg.Index == groupIndex {
|
|
sgi = i
|
|
}
|
|
if sg.Index > maxIndex {
|
|
maxIndex = sg.Index
|
|
}
|
|
}
|
|
if sgi < 0 {
|
|
return e.NewEntityNotExistsError("group #%d", groupIndex)
|
|
}
|
|
|
|
if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil {
|
|
return e.NewShipsBusyError()
|
|
}
|
|
|
|
if g.ShipGroups[sgi].Number < quantity {
|
|
return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity)
|
|
}
|
|
|
|
if quantity == 0 || quantity == g.ShipGroups[sgi].Number {
|
|
g.ShipGroups[sgi].FleetID = nil
|
|
} else {
|
|
newGroup := g.ShipGroups[sgi]
|
|
newGroup.Number = quantity
|
|
g.ShipGroups[sgi].Number -= quantity
|
|
newGroup.Index = maxIndex + 1
|
|
newGroup.FleetID = nil
|
|
g.ShipGroups = append(g.ShipGroups, newGroup)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) joinEqualGroupsInternal(ri int) {
|
|
shipGroups := slices.Collect(maps.Values(maps.Collect(g.listIndexShipGroups(ri))))
|
|
origin := len(shipGroups)
|
|
if origin < 2 {
|
|
return
|
|
}
|
|
for i := 0; i < len(shipGroups)-1; i++ {
|
|
for j := len(shipGroups) - 1; j > i; j-- {
|
|
if shipGroups[i].Equal(shipGroups[j]) {
|
|
shipGroups[i].Index = maxUint(shipGroups[i].Index, shipGroups[j].Index)
|
|
shipGroups[i].Number += shipGroups[j].Number
|
|
shipGroups = append(shipGroups[:j], shipGroups[j+1:]...)
|
|
}
|
|
}
|
|
}
|
|
if len(shipGroups) == origin {
|
|
return
|
|
}
|
|
g.ShipGroups = slices.DeleteFunc(g.ShipGroups, func(v ShipGroup) bool { return v.OwnerID == g.Race[ri].ID })
|
|
g.ShipGroups = append(g.ShipGroups, shipGroups...)
|
|
}
|
|
|
|
func (g *Game) createShips(ri int, shipTypeName string, planetNumber int, quantity int) error {
|
|
st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == shipTypeName })
|
|
if st < 0 {
|
|
return e.NewEntityNotExistsError("ship type %w", shipTypeName)
|
|
}
|
|
pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(planetNumber) })
|
|
if pl < 0 {
|
|
return e.NewEntityNotExistsError("planet #%d", planetNumber)
|
|
}
|
|
if g.Map.Planet[pl].Owner != g.Race[ri].ID {
|
|
return e.NewEntityNotOwnedError("planet %#d", planetNumber)
|
|
}
|
|
|
|
var maxIndex uint
|
|
for _, sg := range g.listIndexShipGroups(ri) {
|
|
if sg.Index > maxIndex {
|
|
maxIndex = sg.Index
|
|
}
|
|
}
|
|
g.ShipGroups = append(g.ShipGroups, ShipGroup{
|
|
Index: maxIndex + 1,
|
|
OwnerID: g.Race[ri].ID,
|
|
TypeID: g.Race[ri].ShipTypes[st].ID,
|
|
Destination: g.Map.Planet[pl].Number,
|
|
Number: uint(quantity),
|
|
State: "In_Orbit",
|
|
Drive: g.Race[ri].Drive,
|
|
Weapons: g.Race[ri].Weapons,
|
|
Shields: g.Race[ri].Shields,
|
|
Cargo: g.Race[ri].Cargo,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (g Game) listShipGroups(ri int) iter.Seq[ShipGroup] {
|
|
return func(yield func(ShipGroup) bool) {
|
|
for _, sg := range g.listIndexShipGroups(ri) {
|
|
if !yield(sg) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g Game) listIndexShipGroups(ri int) iter.Seq2[int, ShipGroup] {
|
|
return func(yield func(int, ShipGroup) bool) {
|
|
for i := range g.ShipGroups {
|
|
if g.ShipGroups[i].OwnerID == g.Race[ri].ID {
|
|
if !yield(i, g.ShipGroups[i]) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func maxUint(a, b uint) uint {
|
|
if b > a {
|
|
return b
|
|
}
|
|
return a
|
|
}
|