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; /// /// 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. /// 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(); }