Files
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

189 lines
6.9 KiB
C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
using Theriapolis.Core;
using Theriapolis.Core.World.Generation;
using Theriapolis.Game.Input;
using Theriapolis.Game.Platform;
using Theriapolis.Game.Rendering;
namespace Theriapolis.Game.Screens;
/// <summary>
/// World-map screen: pan with left-drag or WASD, zoom with mouse wheel.
/// Shows the biome tile map produced by Phase 1 worldgen.
/// A top-left debug overlay shows the world seed and the tile under the cursor;
/// clicking the map copies "seed=N tile=(X,Y)" to the system clipboard.
/// </summary>
public sealed class WorldMapScreen : IScreen
{
private readonly WorldGenContext _ctx;
private Game1 _game = null!;
private Camera2D _camera = null!;
private TileAtlas _atlas = null!;
private WorldMapRenderer _renderer = null!;
private InputManager _input = null!;
private SpriteBatch _sb = null!;
// Debug overlay
private Desktop _overlayDesktop = null!;
private Label _debugLabel = null!;
private int _cursorTileX;
private int _cursorTileY;
// Click-vs-drag detection. Tile is captured at mouse-down so that incidental
// camera pan from hand-jitter between press and release doesn't shift the
// reported tile — at fit-zoom, one screen pixel of drag is ~15 world pixels.
private Vector2 _mouseDownPos;
private int _mouseDownTileX;
private int _mouseDownTileY;
private bool _mouseDownTracked;
private const float ClickSlopPixels = 4f;
public WorldMapScreen(WorldGenContext ctx)
{
_ctx = ctx;
}
public void Initialize(Game1 game)
{
_game = game;
_input = new InputManager();
_sb = new SpriteBatch(game.GraphicsDevice);
var gdw = new GraphicsDeviceWrapper(game.GraphicsDevice);
_camera = new Camera2D(gdw);
// Start camera centred on the world
_camera.Position = new Vector2(
C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS * 0.5f,
C.WORLD_HEIGHT_TILES * C.WORLD_TILE_PIXELS * 0.5f);
// Default zoom: fit the world in the window
float fitZoom = Math.Min(
(float)game.GraphicsDevice.Viewport.Width / (C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS),
(float)game.GraphicsDevice.Viewport.Height / (C.WORLD_HEIGHT_TILES * C.WORLD_TILE_PIXELS));
_camera.AdjustZoom(fitZoom / Camera2D.MinZoom - 1f, new Vector2(
game.GraphicsDevice.Viewport.Width * 0.5f,
game.GraphicsDevice.Viewport.Height * 0.5f));
// Build tile atlas from generated biome defs
_atlas = new TileAtlas(game.GraphicsDevice);
_atlas.GeneratePlaceholders(_ctx.World.BiomeDefs!);
_renderer = new WorldMapRenderer(_ctx, _atlas);
BuildOverlay();
}
private void BuildOverlay()
{
_debugLabel = new Label
{
Text = "",
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(8),
Padding = new Thickness(8, 4, 8, 4),
Background = new SolidBrush(new Color(0, 0, 0, 180)),
};
_overlayDesktop = new Desktop { Root = _debugLabel };
UpdateOverlayText();
}
public void Update(GameTime gameTime)
{
_input.Update();
// Ignore input when the game window isn't focused. Otherwise, clicks on
// other windows (e.g. the Claude desktop app) would still register here
// and overwrite the clipboard with a bogus tile coordinate.
if (!_game.IsActive) return;
// ESC → back to title
if (_input.JustPressed(Keys.Escape))
{
_game.Screens.Pop();
return;
}
float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
float panSpeed = 400f / _camera.Zoom; // world pixels per second
// Keyboard pan (WASD / arrow keys)
Vector2 panDir = Vector2.Zero;
if (_input.IsDown(Keys.W) || _input.IsDown(Keys.Up)) panDir.Y -= 1;
if (_input.IsDown(Keys.S) || _input.IsDown(Keys.Down)) panDir.Y += 1;
if (_input.IsDown(Keys.A) || _input.IsDown(Keys.Left)) panDir.X -= 1;
if (_input.IsDown(Keys.D) || _input.IsDown(Keys.Right)) panDir.X += 1;
if (panDir != Vector2.Zero)
_camera.Pan(panDir * panSpeed * dt);
// Track mouse-down position and tile BEFORE drag-handling consumes the
// frame. Capturing the tile here (rather than re-reading it on release)
// ensures the clipboard reports the tile that was actually clicked,
// independent of any camera pan the click may have incidentally caused.
if (_input.LeftJustDown)
{
_mouseDownPos = _input.MousePosition;
var downWorld = _camera.ScreenToWorld(_input.MousePosition);
_mouseDownTileX = (int)MathF.Floor(downWorld.X / C.WORLD_TILE_PIXELS);
_mouseDownTileY = (int)MathF.Floor(downWorld.Y / C.WORLD_TILE_PIXELS);
_mouseDownTracked = true;
}
// Mouse drag pan
var dragDelta = _input.ConsumeDragDelta(_camera);
if (dragDelta != Vector2.Zero)
_camera.Pan(dragDelta);
// Mouse wheel zoom
int scroll = _input.ScrollDelta;
if (scroll != 0)
{
float zoomDelta = scroll > 0 ? 0.12f : -0.12f;
_camera.AdjustZoom(zoomDelta, _input.MousePosition);
}
// Resolve cursor → tile coordinate for overlay + click handler
var worldPos = _camera.ScreenToWorld(_input.MousePosition);
_cursorTileX = (int)MathF.Floor(worldPos.X / C.WORLD_TILE_PIXELS);
_cursorTileY = (int)MathF.Floor(worldPos.Y / C.WORLD_TILE_PIXELS);
// On release without drag, copy debug info to clipboard. Use the tile
// captured at mouse-down so hand-jitter between press and release can't
// shift the reported tile via incidental camera pan.
if (_input.LeftJustUp && _mouseDownTracked)
{
_mouseDownTracked = false;
if (Vector2.Distance(_input.MousePosition, _mouseDownPos) <= ClickSlopPixels)
Clipboard.TrySetText($"seed={_ctx.World.WorldSeed} tile=({_mouseDownTileX},{_mouseDownTileY})");
}
UpdateOverlayText();
}
private void UpdateOverlayText()
{
_debugLabel.Text =
$"Seed: {_ctx.World.WorldSeed}\n" +
$"Tile: ({_cursorTileX}, {_cursorTileY})";
}
public void Draw(GameTime gameTime, SpriteBatch _)
{
_game.GraphicsDevice.Clear(new Color(5, 10, 20));
_renderer.Draw(_sb, _camera, gameTime);
_overlayDesktop.Render();
}
public void Deactivate() { }
public void Reactivate() { }
// Dispose rendering resources when screen is removed
~WorldMapScreen() => (_renderer as IDisposable)?.Dispose();
}