package calculator import ( "errors" "galaxy/calc" "galaxy/client/widget/numeric" "galaxy/util" "slices" "strconv" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/widget" ) type CalculatorOpt func(*Calculator) type ShipClass struct { Name string Drive float64 Armament uint Weapons float64 Shields float64 Cargo float64 } type ShipClassFn func(string, float64, uint, float64, float64, float64) type Calculator struct { CanvasObject fyne.CanvasObject playerDrivesTech float64 playerWeaponsTech float64 playerShieldsTech float64 playerCargoTech float64 shipDriveEntry *numeric.FloatEntry shipWeaponsEntry *numeric.FloatEntry shipArmamentEntry *numeric.IntEntry shipShieldsEntry *numeric.FloatEntry shipCargoEntry *numeric.FloatEntry playerDrivesTechEntry *numeric.FloatEntry playerWeaponsTechEntry *numeric.FloatEntry playerShieldsTechEntry *numeric.FloatEntry playerCargoTechEntry *numeric.FloatEntry drivesTechOverride *widget.Check weaponsTechOverride *widget.Check shieldsTechOverride *widget.Check cargoTechOverride *widget.Check massEntry *numeric.FloatEntry speedEntry *numeric.FloatEntry attackEntry *numeric.FloatEntry defenseEntry *numeric.FloatEntry cargoLoadEntry *numeric.FloatEntry planetMatEntry *numeric.FloatEntry massOverride *widget.Check speedOverride *widget.Check attackOverride *widget.Check defenseOverride *widget.Check cargoLoadMaximize *widget.Check planetMatOverride *widget.Check planetLabel *widget.Label planetMassProdLabel *widget.Label planetShipsProdLabel *widget.Label planetContainer fyne.CanvasObject planetProdContainer fyne.CanvasObject shipSelector *widget.SelectEntry shipCreateButton *widget.Button onCreateHandler ShipClassFn loader ShipClassFn knownClasses []ShipClass validateMu sync.RWMutex l, mat, res float64 Valid bool } func WithPlayerDrives(v float64) CalculatorOpt { return func(c *Calculator) { c.playerDrivesTech = v } } func WithPlayerWeapons(v float64) CalculatorOpt { return func(c *Calculator) { c.playerWeaponsTech = v } } func WithPlayerShields(v float64) CalculatorOpt { return func(c *Calculator) { c.playerShieldsTech = v } } func WithPlayerCargo(v float64) CalculatorOpt { return func(c *Calculator) { c.playerCargoTech = v } } func WithCreateHandler(f ShipClassFn) CalculatorOpt { return func(c *Calculator) { c.onCreateHandler = f } } func NewCaclulator(opts ...CalculatorOpt) *Calculator { c := &Calculator{} c.shipCreateButton = widget.NewButton(lang.L("ship.action.create"), c.onCreateShipClassButton) c.shipCreateButton.Disable() c.loader = c.LoadShipClass c.planetMatEntry = numeric.NewFloatEntry(10, c.onPlanetMatChange) c.planetMatOverride = widget.NewCheck("", c.overridePlanetMat) c.planetMatOverride.Disable() c.planetLabel = widget.NewLabel("") c.planetMassProdLabel = bareLabel("") c.planetShipsProdLabel = bareLabel("") c.planetProdContainer = container.NewHBox( label(lang.L("planet.prod.mass")+":"), fixedLabel(c.planetMassProdLabel, 80), label(lang.L("planet.prod.ships")+":"), fixedLabel(c.planetShipsProdLabel, 80), ) c.planetProdContainer.Hide() c.planetContainer = container.NewVBox( widget.NewSeparator(), container.NewHBox(c.planetLabel), rowForItem(lang.L("planet.mat")+":", floatEntry(c.planetMatEntry, 100), c.planetMatOverride), c.planetProdContainer, ) c.planetContainer.Hide() c.shipSelector = widget.NewSelectEntry(nil) c.shipSelector.OnChanged = c.onShipSelectorChange c.shipDriveEntry = numeric.NewFloatEntry(7, c.onShipDriveChange) c.shipWeaponsEntry = numeric.NewFloatEntry(7, c.onShipWeaponsChange) c.shipArmamentEntry = numeric.NewIntEntry(7, c.onShipArmamentChange) c.shipShieldsEntry = numeric.NewFloatEntry(7, c.onShipShieldsChange) c.shipCargoEntry = numeric.NewFloatEntry(7, c.onShipCargoChange) c.playerDrivesTechEntry = numeric.NewFloatEntry(7, c.onDrivesTechChange) c.playerWeaponsTechEntry = numeric.NewFloatEntry(7, c.onWeaponsTechChange) c.playerShieldsTechEntry = numeric.NewFloatEntry(7, c.onShieldsTechChange) c.playerCargoTechEntry = numeric.NewFloatEntry(7, c.onCargoTechChange) c.massEntry = numeric.NewFloatEntry(7, c.onMassChange) c.speedEntry = numeric.NewFloatEntry(7, c.onSpeedChange) c.attackEntry = numeric.NewFloatEntry(7, c.onAttackChange) c.defenseEntry = numeric.NewFloatEntry(7, c.onDefenseChange) c.cargoLoadEntry = numeric.NewFloatEntry(7, c.onCargoLoadChange) c.drivesTechOverride = widget.NewCheck("", c.overrideDrivesTech) c.drivesTechOverride.Disable() c.weaponsTechOverride = widget.NewCheck("", c.overrideWeaponsTech) c.weaponsTechOverride.Disable() c.shieldsTechOverride = widget.NewCheck("", c.overrideShieldsTech) c.shieldsTechOverride.Disable() c.cargoTechOverride = widget.NewCheck("", c.overrideCargoTech) c.cargoTechOverride.Disable() c.massOverride = widget.NewCheck("", c.overrideMass) c.massOverride.Disable() c.speedOverride = widget.NewCheck("", c.overrideSpeed) c.speedOverride.Disable() c.attackOverride = widget.NewCheck("", c.overrideAttack) c.attackOverride.Disable() c.defenseOverride = widget.NewCheck("", c.overrideDefense) c.defenseOverride.Disable() c.cargoLoadMaximize = widget.NewCheck(lang.L("label.max"), c.maximizeCargoLoad) c.cargoLoadMaximize.SetChecked(true) createShip := container.NewBorder( nil, // top nil, // bottom nil, // left c.shipCreateButton, // right c.shipSelector, // center ) c.CanvasObject = container.NewVBox( container.NewPadded(createShip), widget.NewSeparator(), rowForTech(lang.L("tech.d")+":", c.shipDriveEntry, floatEntry(c.playerDrivesTechEntry, 80), c.drivesTechOverride), rowForWeapons(lang.L("tech.w")+":", c.shipArmamentEntry, c.shipWeaponsEntry, floatEntry(c.playerWeaponsTechEntry, 80), c.weaponsTechOverride), rowForTech(lang.L("tech.s")+":", c.shipShieldsEntry, floatEntry(c.playerShieldsTechEntry, 80), c.shieldsTechOverride), rowForTech(lang.L("tech.c")+":", c.shipCargoEntry, floatEntry(c.playerCargoTechEntry, 80), c.cargoTechOverride), widget.NewSeparator(), rowForItem(lang.L("ship.load")+":", floatEntry(c.cargoLoadEntry, 80), c.cargoLoadMaximize), rowForItem(lang.L("ship.mass")+":", floatEntry(c.massEntry, 80), c.massOverride), rowForItem(lang.L("ship.speed")+":", floatEntry(c.speedEntry, 80), c.speedOverride), rowForItem(lang.L("ship.attack")+":", floatEntry(c.attackEntry, 80), c.attackOverride), rowForItem(lang.L("ship.defense")+":", floatEntry(c.defenseEntry, 80), c.defenseOverride), c.planetContainer, ) c.Init(opts...) return c } func (c *Calculator) Init(opts ...CalculatorOpt) { for i := range opts { opts[i](c) } c.playerDrivesTechEntry.SetOrigin(c.playerDrivesTech) c.playerWeaponsTechEntry.SetOrigin(c.playerWeaponsTech) c.playerShieldsTechEntry.SetOrigin(c.playerShieldsTech) c.playerCargoTechEntry.SetOrigin(c.playerCargoTech) c.CanvasObject.Show() } func (c *Calculator) Refresh() { c.validate() c.CanvasObject.Refresh() } func (c *Calculator) RegisterClasses(shipClass ...ShipClass) { c.knownClasses = shipClass names := make([]string, len(c.knownClasses)) for i := range c.knownClasses { names[i] = c.knownClasses[i].Name } slices.Sort(names) c.shipSelector = widget.NewSelectEntry(names) c.shipSelector.OnChanged = c.onShipSelectorChange } func (c *Calculator) onCreateShipClassButton() { if c.onCreateHandler == nil || !c.Valid { return } } func (c *Calculator) validate() { fyne.Do(func() { c.validateMu.Lock() err := c.validateEntries() c.Valid = err == nil if err != nil { } else { } c.shipClassNameValidate() c.validateMu.Unlock() }) } func (c *Calculator) validateEntries() (err error) { defer func() { if err != nil { c.cargoLoadEntry.Clear() if !c.massOverride.Checked { c.massEntry.Clear() } if !c.speedOverride.Checked { c.speedEntry.Clear() } if !c.attackOverride.Checked { c.attackEntry.Clear() } if !c.defenseOverride.Checked { c.defenseEntry.Clear() } // c.planetProdContainer.Hide() } }() drive, ok := c.shipDriveEntry.Value() if !ok { err = errors.New("Parameter Drive is not valid") return } driveTech, ok := c.playerDrivesTechEntry.Value() if !ok { err = errors.New("Drive tech level is not valid") return } armament, ok := c.shipArmamentEntry.Value() if !ok { err = errors.New("Parameter Armament is not valid") return } weapons, ok := c.shipWeaponsEntry.Value() if !ok { err = errors.New("Parameter Weapons is not valid") return } weaponsTech, ok := c.playerWeaponsTechEntry.Value() if !ok { err = errors.New("Weapons tech level is not valid") return } shields, ok := c.shipShieldsEntry.Value() if !ok { err = errors.New("Parameter Shields is not valid") return } shieldsTech, ok := c.playerShieldsTechEntry.Value() if !ok { err = errors.New("Shields tech level is not valid") return } cargo, ok := c.shipCargoEntry.Value() if !ok { err = errors.New("Parameter Cargo is not valid") return } cargoTech, ok := c.playerCargoTechEntry.Value() if !ok { err = errors.New("Cargo tech level is not valid") return } err = calc.ValidateShipTypeValues(drive, armament, weapons, shields, cargo) if err != nil { return } var cargoLoad float64 if c.cargoLoadMaximize.Checked { cargoLoad = calc.CargoCapacity(cargo, cargoTech) c.cargoLoadEntry.SetOrigin(cargoLoad) } else if cargoLoad, ok = c.cargoLoadEntry.Value(); !ok { err = errors.New("Cargo load value is not valid") return } emptyMass, ok := calc.EmptyMass(drive, weapons, uint(armament), shields, cargo) if !ok { err = errors.New("Unable to calculate empty mass (check armament and weapons)") return } fullMass := calc.FullMass(emptyMass, cargoLoad) speed := calc.Speed(calc.DriveEffective(drive, driveTech), fullMass) effectiveAttack := calc.EffectiveAttack(weapons, weaponsTech) effectiveDefense := calc.EffectiveDefence(shields, shieldsTech, fullMass) c.massEntry.SetOrigin(emptyMass) c.speedEntry.SetOrigin(speed) c.attackEntry.SetOrigin(effectiveAttack) c.defenseEntry.SetOrigin(effectiveDefense) planetMat, ok := c.planetMatEntry.Value() if !ok { // c.planetProdContainer.Hide() } else { massProd := calc.PlanetProduceShipMass(c.l, planetMat, c.res) c.planetMassProdLabel.SetText(strconv.FormatFloat(util.Fixed3(massProd), 'f', -1, 64)) ships := 0. if emptyMass > 0 { ships = massProd / emptyMass } c.planetShipsProdLabel.SetText(strconv.FormatFloat(util.Fixed3(ships), 'f', -1, 64)) c.planetProdContainer.Show() } return } func (c *Calculator) onOriginInputChange(cb *widget.Check, e *numeric.FloatEntry) { if e == nil { return } if cb != nil { cb.Checked = e.Overriden() if !cb.Checked { cb.Disable() } else { cb.Enable() } } c.onFloatEntryChange(e) } func (c *Calculator) onFloatEntryChange(e *numeric.FloatEntry) { if e == nil { return } e.Validate() c.validate() } func (c *Calculator) onIntEntryChange(e *numeric.IntEntry) { if e == nil { return } e.Validate() c.validate() } func (c *Calculator) overrideChecked(cb *widget.Check, e *numeric.FloatEntry) { if cb == nil || e == nil { return } if !cb.Checked { e.Reset() cb.Disable() } } func (c *Calculator) onShipDriveChange(string) { c.onFloatEntryChange(c.shipDriveEntry) } func (c *Calculator) onShipArmamentChange(string) { defer c.onIntEntryChange(c.shipArmamentEntry) if weapons, ok := c.shipWeaponsEntry.Value(); !ok || !c.shipWeaponsEntry.Valid { return } else if armament, ok := c.shipArmamentEntry.Value(); !ok || !c.shipArmamentEntry.Valid { return } else if armament > 0 && weapons == 0 { c.shipWeaponsEntry.SetOrigin(1.0) } else if armament == 0 && weapons > 0 { c.shipWeaponsEntry.SetOrigin(0.0) } } func (c *Calculator) onShipWeaponsChange(string) { defer c.onFloatEntryChange(c.shipWeaponsEntry) if weapons, ok := c.shipWeaponsEntry.Value(); !ok || !c.shipWeaponsEntry.Valid { return } else if armament, ok := c.shipArmamentEntry.Value(); !ok || !c.shipArmamentEntry.Valid { return } else if weapons > 0 && armament == 0 { c.shipArmamentEntry.SetOrigin(1) } else if weapons == 0 && armament > 0 { c.shipArmamentEntry.SetOrigin(0) } } func (c *Calculator) onShipShieldsChange(string) { c.onFloatEntryChange(c.shipShieldsEntry) } func (c *Calculator) onShipCargoChange(string) { c.onFloatEntryChange(c.shipCargoEntry) } func (c *Calculator) onDrivesTechChange(string) { c.onOriginInputChange(c.drivesTechOverride, c.playerDrivesTechEntry) } func (c *Calculator) overrideDrivesTech(bool) { c.overrideChecked(c.drivesTechOverride, c.playerDrivesTechEntry) } func (c *Calculator) onWeaponsTechChange(string) { c.onOriginInputChange(c.weaponsTechOverride, c.playerWeaponsTechEntry) } func (c *Calculator) overrideWeaponsTech(bool) { c.overrideChecked(c.weaponsTechOverride, c.playerWeaponsTechEntry) } func (c *Calculator) onShieldsTechChange(string) { c.onOriginInputChange(c.shieldsTechOverride, c.playerShieldsTechEntry) } func (c *Calculator) overrideShieldsTech(bool) { c.overrideChecked(c.shieldsTechOverride, c.playerShieldsTechEntry) } func (c *Calculator) onCargoTechChange(string) { c.onOriginInputChange(c.cargoTechOverride, c.playerCargoTechEntry) } func (c *Calculator) overrideCargoTech(bool) { c.overrideChecked(c.cargoTechOverride, c.playerCargoTechEntry) } func (c *Calculator) onCargoLoadChange(string) { c.onFloatEntryChange(c.cargoLoadEntry) } func (c *Calculator) onMassChange(string) { c.onOriginInputChange(c.massOverride, c.massEntry) } func (c *Calculator) overrideMass(bool) { c.overrideChecked(c.massOverride, c.massEntry) } func (c *Calculator) onSpeedChange(string) { c.onOriginInputChange(c.speedOverride, c.speedEntry) } func (c *Calculator) overrideSpeed(bool) { c.overrideChecked(c.speedOverride, c.speedEntry) } func (c *Calculator) onAttackChange(string) { c.onOriginInputChange(c.attackOverride, c.attackEntry) } func (c *Calculator) overrideAttack(bool) { c.overrideChecked(c.attackOverride, c.attackEntry) } func (c *Calculator) onDefenseChange(string) { c.onOriginInputChange(c.defenseOverride, c.defenseEntry) } func (c *Calculator) overrideDefense(bool) { c.overrideChecked(c.defenseOverride, c.defenseEntry) } func (c *Calculator) maximizeCargoLoad(bool) { c.validate() } func (c *Calculator) onPlanetMatChange(string) { c.onOriginInputChange(c.planetMatOverride, c.planetMatEntry) } func (c *Calculator) overridePlanetMat(bool) { c.overrideChecked(c.planetMatOverride, c.planetMatEntry) } func (c *Calculator) onShipSelectorChange(v string) { i, ok := c.shipClassNameValidate() if i < 0 || !ok || c.loader == nil { return } c.loader( c.knownClasses[i].Name, c.knownClasses[i].Drive, c.knownClasses[i].Armament, c.knownClasses[i].Weapons, c.knownClasses[i].Shields, c.knownClasses[i].Cargo, ) } func (c *Calculator) shipClassNameValidate() (int, bool) { var canCreateShip bool defer func() { if canCreateShip && c.Valid { c.shipCreateButton.Enable() } else { c.shipCreateButton.Disable() } }() name, canCreateShip := util.ValidateTypeName(c.shipSelector.Text) if canCreateShip { c.shipSelector.Text = name } i := slices.IndexFunc(c.knownClasses, func(v ShipClass) bool { return v.Name == name }) canCreateShip = canCreateShip && i < 0 return i, canCreateShip } func (c *Calculator) LoadShipClass(n string, D float64, A uint, W float64, S float64, C float64) { c.shipDriveEntry.SetOrigin(D) c.shipArmamentEntry.SetOrigin(int(A)) c.shipWeaponsEntry.SetOrigin(W) c.shipShieldsEntry.SetOrigin(S) c.shipCargoEntry.SetOrigin(C) } func rowForItem(l string, entry, override fyne.CanvasObject) fyne.CanvasObject { i := []fyne.CanvasObject{label(l), entry} if override != nil { i = append(i, override) } return container.NewHBox(i...) } func rowForTech(l string, shipEntry, techEntry, btn fyne.CanvasObject) fyne.CanvasObject { return container.NewHBox( label(l), floatEntry(shipEntry, 115), widget.NewLabel("@"), techEntry, btn, ) } func rowForWeapons(l string, armamentEntry, weaponsEntry, techEntry, btn fyne.CanvasObject) fyne.CanvasObject { return container.NewHBox( label(l), intEntry(armamentEntry, 35), floatEntry(weaponsEntry, 75), widget.NewLabel("@"), techEntry, btn, ) } func label(l string) fyne.CanvasObject { return fixedLabel(bareLabel(l), 110) } func fixedLabel(w *widget.Label, width float32) fyne.CanvasObject { s := container.NewHScroll(w) s.SetMinSize(fyne.NewSize(width, 1)) return s } func bareLabel(l string) *widget.Label { w := widget.NewLabelWithStyle(l, fyne.TextAlignTrailing, fyne.TextStyle{Monospace: true, Symbol: false}) w.Selectable = false w.Truncation = fyne.TextTruncateOff return w } func intEntry(content fyne.CanvasObject, width float32) fyne.CanvasObject { s := container.NewHScroll(content) s.SetMinSize(fyne.NewSize(width, 1)) return s } func floatEntry(content fyne.CanvasObject, width float32) fyne.CanvasObject { s := container.NewHScroll(content) s.SetMinSize(fyne.NewSize(width, 1)) return s }