Files
TheriapolisV3/Theriapolis.Game/Rendering/Camera2D.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

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