b451f83174
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>
189 lines
6.9 KiB
C#
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();
|
|
}
|