From d9a54f1319cc9dcc2861cecc9edfde14cf039c5b Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Sat, 6 Jun 2026 20:59:12 +0200 Subject: Add examples catalog with subcommand dispatch - Extract game engine from package main to engine/ library package - Create examples/ package with registry, Entry type, and BuilderDeps - Register default world as an example (moved from main()) - Add combat example: two teams of units facing each other - Rewrite main.go as thin dispatch: go run . example [name] - Update README with examples catalog one-liner --- engine/camera.go | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 engine/camera.go (limited to 'engine/camera.go') diff --git a/engine/camera.go b/engine/camera.go new file mode 100644 index 0000000..867aaf4 --- /dev/null +++ b/engine/camera.go @@ -0,0 +1,174 @@ +package engine + +import ( + "math" + + "age_of_empires/physics" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +const ( + ScreenWidth = 960 + ScreenHeight = 640 + WorldWidth = 3200 + WorldHeight = 2400 + + cameraZoomMin = 0.25 + cameraZoomMax = 3.0 + cameraZoomStep = 0.10 + cameraScrollSpeed = 400.0 + cameraDrag = 8.0 +) + +type Camera struct { + X, Y float64 + Zoom float64 + + vx, vy float64 + + dragActive bool + dragStartScreenX, dragStartScreenY int + dragStartCamX, dragStartCamY float64 +} + +func (c *Camera) WorldToScreen(p physics.Point) (float64, float64) { + sx := (float64(p.X)-c.X)*c.Zoom + ScreenWidth/2 + sy := (float64(p.Y)-c.Y)*c.Zoom + ScreenHeight/2 + return sx, sy +} + +func (c *Camera) ScreenToWorld(sx, sy int) physics.Point { + zoom := c.Zoom + if zoom == 0 { + zoom = 1 + } + wx := float64(sx-ScreenWidth/2)/zoom + c.X + wy := float64(sy-ScreenHeight/2)/zoom + c.Y + return physics.Point{X: int(math.Round(wx)), Y: int(math.Round(wy))} +} + +func (c *Camera) GeoM() ebiten.GeoM { + var m ebiten.GeoM + m.Translate(-c.X, -c.Y) + m.Scale(c.Zoom, c.Zoom) + m.Translate(ScreenWidth/2, ScreenHeight/2) + return m +} + +func (c *Camera) clampToBounds() { + halfW := (ScreenWidth / 2.0) / c.Zoom + halfH := (ScreenHeight / 2.0) / c.Zoom + + minX, maxX := halfW, float64(WorldWidth)-halfW + if minX > maxX { + minX = WorldWidth / 2 + maxX = WorldWidth / 2 + } + minY, maxY := halfH, float64(WorldHeight)-halfH + if minY > maxY { + minY = WorldHeight / 2 + maxY = WorldHeight / 2 + } + + if c.X < minX { + c.X = minX + if c.vx < 0 { + c.vx = 0 + } + } + if c.X > maxX { + c.X = maxX + if c.vx > 0 { + c.vx = 0 + } + } + if c.Y < minY { + c.Y = minY + if c.vy < 0 { + c.vy = 0 + } + } + if c.Y > maxY { + c.Y = maxY + if c.vy > 0 { + c.vy = 0 + } + } +} + +func (c *Camera) Update(dt float64) { + zoom := c.Zoom + if zoom == 0 { + zoom = 1 + } + + // Scroll wheel zoom toward cursor + _, wy := ebiten.Wheel() + if wy != 0 { + mx, my := ebiten.CursorPosition() + // cursor world pos before zoom + wx0 := float64(mx-ScreenWidth/2)/zoom + c.X + wy0 := float64(my-ScreenHeight/2)/zoom + c.Y + + newZoom := math.Max(cameraZoomMin, math.Min(cameraZoomMax, zoom+wy*cameraZoomStep)) + c.Zoom = newZoom + zoom = newZoom + + // translate so cursor stays fixed + wx1 := float64(mx-ScreenWidth/2)/zoom + c.X + wy1 := float64(my-ScreenHeight/2)/zoom + c.Y + c.X -= wx1 - wx0 + c.Y -= wy1 - wy0 + } + + // Middle-mouse drag + mx, my := ebiten.CursorPosition() + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonMiddle) { + c.dragActive = true + c.dragStartScreenX = mx + c.dragStartScreenY = my + c.dragStartCamX = c.X + c.dragStartCamY = c.Y + } + if c.dragActive { + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) { + c.X = c.dragStartCamX - float64(mx-c.dragStartScreenX)/zoom + c.Y = c.dragStartCamY - float64(my-c.dragStartScreenY)/zoom + } else { + c.dragActive = false + } + } + + // Arrow key pan (velocity-based) + dx, dy := 0.0, 0.0 + if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { + dx -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyArrowRight) { + dx += 1 + } + if ebiten.IsKeyPressed(ebiten.KeyArrowUp) { + dy -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyArrowDown) { + dy += 1 + } + if dx != 0 && dy != 0 { + inv := 1.0 / math.Sqrt2 + dx *= inv + dy *= inv + } + c.vx += dx * cameraScrollSpeed + c.vy += dy * cameraScrollSpeed + + // Apply velocity and exponential drag + c.X += c.vx * dt + c.Y += c.vy * dt + decay := math.Exp(-cameraDrag * dt) + c.vx *= decay + c.vy *= decay + + c.clampToBounds() +} -- cgit v1.3