96 lines
3.5 KiB
C#
96 lines
3.5 KiB
C#
|
|
using Microsoft.Xna.Framework;
|
||
|
|
|
||
|
|
namespace Theriapolis.Game.Rendering;
|
||
|
|
|
||
|
|
public enum ViewMode { WorldMap, Tactical }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 2D orthographic camera. Position is in world-pixel space.
|
||
|
|
/// Both WorldMap and Tactical views share the same camera; only the renderer changes.
|
||
|
|
/// </summary>
|
||
|
|
public sealed class Camera2D
|
||
|
|
{
|
||
|
|
private readonly GraphicsDeviceWrapper _gd;
|
||
|
|
|
||
|
|
/// <summary>Camera position in world-pixel space (top-left of view at zoom 1).</summary>
|
||
|
|
public Vector2 Position { get; set; } = Vector2.Zero;
|
||
|
|
|
||
|
|
/// <summary>Zoom level. 1.0 = 1 world pixel per screen pixel.</summary>
|
||
|
|
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;
|
||
|
|
|
||
|
|
/// <summary>SpriteBatch transform matrix for this camera.</summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Returns the visible rectangle in world-tile coordinates.
|
||
|
|
/// </summary>
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Thin wrapper so Camera2D doesn't reference the MonoGame GraphicsDevice directly.</summary>
|
||
|
|
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;
|
||
|
|
}
|