Files
galaxy-game/client/client.go
T
2026-03-14 19:47:47 +02:00

216 lines
4.9 KiB
Go

package client
import (
"context"
"image"
"sync"
"galaxy/client/world"
"galaxy/connector"
mc "galaxy/model/client"
"galaxy/model/report"
"galaxy/storage"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
const version = "1.0.0"
type client struct {
s storage.UIStorage
conn connector.UIConnector
app fyne.App
window fyne.Window
loadReportFunc func(uint)
world *world.World
drawer *world.GGDrawer
raster *canvas.Raster
co *RasterCoalescer[world.RenderParams]
pan *PanController
// Protected camera/options state (UI-facing). This is the "base" params snapshot.
// Viewport/margins are NOT stored here; they come from raster draw callback.
mu sync.RWMutex
wp *world.RenderParams
canvasScale float32
// Latest raster geometry metadata for correct event->pixel conversion:
// - logical size: raster.Size() (Fyne units)
// - pixel size: last (wPx,hPx) passed to draw callback
metaMu sync.RWMutex
lastRasterLogicW float32
lastRasterLogicH float32
lastRasterPxW int
lastRasterPxH int
lastCanvasScale float32 // optional, useful for debugging
// Snapshot of params actually used for the last render (includes viewport/margins).
// Used for HitTest and to keep UI interactions consistent with what the user sees.
lastRenderedMu sync.RWMutex
lastRenderedParams world.RenderParams
// Indexing / backing-canvas caches (owned by client because it depends on UI geometry)
lastIndexedViewportW int
lastIndexedViewportH int
lastIndexedZoomFp int
lastCanvasW int
lastCanvasH int
viewportImg *image.RGBA
viewportW int
viewportH int
hits []world.Hit
}
func NewClient(ctx context.Context, s storage.UIStorage, conn connector.UIConnector, app fyne.App) (mc.Client, error) {
e := &client{
s: s,
conn: conn,
app: app,
window: app.NewWindow("Galaxy Plus"),
world: nil,
wp: &world.RenderParams{
CameraZoom: 1.0,
Options: &world.RenderOptions{DisableWrapScroll: false},
},
lastCanvasScale: 1.0,
hits: make([]world.Hit, 5),
}
e.loadReportFunc = func(t uint) { e.loadReport(ctx, t) }
e.drawer = &world.GGDrawer{DC: nil}
e.raster = canvas.NewRaster(func(wPx, hPx int) image.Image {
return e.draw(wPx, hPx)
})
e.pan = NewPanController(e)
e.co = NewRasterCoalescer(
FyneExecutor{},
e.raster,
func(wPx, hPx int, p world.RenderParams) image.Image {
return e.renderRasterImage(wPx, hPx, p)
},
)
return e, nil
}
func (e *client) loadReport(ctx context.Context, t uint) {
e.conn.FetchReport("GAME_ID", t, func(r report.Report, err error) {
if err != nil {
e.handlerError(err)
} else {
e.setReport(r)
}
})
}
func (e *client) setReport(r report.Report) {
w := world.NewWorld(int(r.Width), int(r.Height))
for i := range r.LocalPlanet {
p := r.LocalPlanet[i]
w.AddCircle(p.X.F(), p.Y.F(), p.Size.F())
}
for i := range r.UnidentifiedPlanet {
p := r.UnidentifiedPlanet[i]
w.AddPoint(p.X.F(), p.Y.F())
}
e.loadWorld(w)
}
func (e *client) handlerError(err error) {
}
func (e *client) BuildUI(w fyne.Window) {
mapCanvas := newInteractiveRaster(e, e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
mapCanvas.SetMinSize(fyne.NewSize(292, 292))
toolbar := widget.NewToolbar(
widget.NewToolbarAction(
theme.FolderIcon(),
func() { e.loadWorld(mockWorld()) }),
widget.NewToolbarSeparator(),
widget.NewToolbarAction(
theme.NavigateBackIcon(),
func() {}),
widget.NewToolbarAction(
theme.NavigateNextIcon(),
func() {}),
)
tabs := container.NewAppTabs(
container.NewTabItemWithIcon(
"Map",
theme.GridIcon(),
mapCanvas),
container.NewTabItemWithIcon(
"Calculator",
theme.ComputerIcon(),
container.NewStack(widget.NewButton("Calc", func() {})),
),
)
content := container.NewBorder(
toolbar, // top
nil, // bottom
nil, // left
nil, // right
tabs, // center
)
w.CenterOnScreen()
w.SetContent(content)
}
func (e *client) loadWorld(w *world.World) {
w.SetCircleRadiusScaleFp(world.SCALE / 4)
e.world = w
// TODO: store camera position in user settings
e.wp.CameraXWorldFp = w.W / 2
e.wp.CameraYWorldFp = w.H / 2
e.world.SetTheme(world.ThemeDark)
// if e.world == nil {
// w.SetCircleRadiusScaleFp(world.SCALE / 4)
// e.world = w
// e.wp.CameraXWorldFp = w.W / 2
// e.wp.CameraYWorldFp = w.H / 2
// e.world.SetTheme(world.ThemeDark)
// } else {
// if e.world.Theme().ID() == "theme.light.v1" {
// e.world.SetTheme(world.ThemeDark)
// } else {
// e.world.SetTheme(world.ThemeLight)
// }
// }
e.RequestRefresh()
}
func (e *client) Run() error {
e.BuildUI(e.window)
e.window.ShowAndRun()
e.RequestRefresh()
return nil
}
func (e *client) Shutdown() {
e.window.Close()
}
func (e *client) OnConnection(bool) {}
func (e *client) Version() string { return version }