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;
}