using Microsoft.Xna.Framework; namespace Theriapolis.Game.Rendering; public enum ViewMode { WorldMap, Tactical } /// /// 2D orthographic camera. Position is in world-pixel space. /// Both WorldMap and Tactical views share the same camera; only the renderer changes. /// public sealed class Camera2D { private readonly GraphicsDeviceWrapper _gd; /// Camera position in world-pixel space (top-left of view at zoom 1). public Vector2 Position { get; set; } = Vector2.Zero; /// Zoom level. 1.0 = 1 world pixel per screen pixel. public float Zoom { get; private set; } = 1f / Theriapolis.Core.C.WORLD_TILE_PIXELS; public ViewMode Mode { get; set; } = ViewMode.WorldMap; // Re-exports of the canonical zoom constants in C.* so existing call sites // (Camera2D.MinZoom, etc.) keep working without churn. public const float MinZoom = Theriapolis.Core.C.CAMERA_MIN_ZOOM; public const float MaxZoom = Theriapolis.Core.C.CAMERA_MAX_ZOOM; public const float TacticalThreshold = Theriapolis.Core.C.CAMERA_TACTICAL_THRESHOLD; public Camera2D(GraphicsDeviceWrapper gd) { _gd = gd; } public int ScreenWidth => _gd.Width; public int ScreenHeight => _gd.Height; /// SpriteBatch transform matrix for this camera. public Matrix TransformMatrix => Matrix.CreateTranslation(-Position.X, -Position.Y, 0f) * Matrix.CreateScale(Zoom, Zoom, 1f) * Matrix.CreateTranslation(ScreenWidth * 0.5f, ScreenHeight * 0.5f, 0f); public Vector2 WorldToScreen(Vector2 world) { var v = Vector2.Transform(world, TransformMatrix); return v; } public Vector2 ScreenToWorld(Vector2 screen) { var inv = Matrix.Invert(TransformMatrix); return Vector2.Transform(screen, inv); } public void AdjustZoom(float delta, Vector2 screenFocus) { // Keep the world point under screenFocus stationary var worldFocus = ScreenToWorld(screenFocus); Zoom = Math.Clamp(Zoom * (1f + delta), MinZoom, MaxZoom); var newScreen = WorldToScreen(worldFocus); Position += (screenFocus - newScreen) / Zoom; // Update view mode based on zoom threshold Mode = Zoom >= TacticalThreshold ? ViewMode.Tactical : ViewMode.WorldMap; } public void Pan(Vector2 worldDelta) { Position += worldDelta; } /// /// Returns the visible rectangle in world-tile coordinates. /// public (int x0, int y0, int x1, int y1) VisibleTileRect() { var tl = ScreenToWorld(Vector2.Zero); var br = ScreenToWorld(new Vector2(ScreenWidth, ScreenHeight)); int px = Theriapolis.Core.C.WORLD_TILE_PIXELS; int x0 = Math.Max(0, (int)MathF.Floor(tl.X / px)); int y0 = Math.Max(0, (int)MathF.Floor(tl.Y / px)); int x1 = Math.Min(Theriapolis.Core.C.WORLD_WIDTH_TILES - 1, (int)MathF.Ceiling(br.X / px)); int y1 = Math.Min(Theriapolis.Core.C.WORLD_HEIGHT_TILES - 1, (int)MathF.Ceiling(br.Y / px)); return (x0, y0, x1, y1); } } /// Thin wrapper so Camera2D doesn't reference the MonoGame GraphicsDevice directly. public sealed class GraphicsDeviceWrapper { private readonly Microsoft.Xna.Framework.Graphics.GraphicsDevice _device; public int Width => _device.Viewport.Width; public int Height => _device.Viewport.Height; public GraphicsDeviceWrapper(Microsoft.Xna.Framework.Graphics.GraphicsDevice device) => _device = device; }