ui calculator

This commit is contained in:
Ilia Denisov
2026-03-30 19:38:24 +02:00
committed by GitHub
parent 17f366cd6b
commit a7793f5416
37 changed files with 2046 additions and 270 deletions
+629
View File
@@ -0,0 +1,629 @@
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
}