Minimap!
This commit is contained in:
parent
f7720a9915
commit
6c37e569ab
18 changed files with 1169 additions and 323 deletions
247
Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs
Normal file
247
Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
// Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
using TD.Levels;
|
||||
|
||||
namespace TD.UI.Minimap
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the static terrain layer of the WC3-style minimap as a <see cref="Texture2D"/>
|
||||
/// from the baked <see cref="LevelData"/>. One pixel per tile; the UI element scales the
|
||||
/// texture up with Point filtering so the result reads as crisp, chunky map abstraction
|
||||
/// rather than a tiny blurred photograph.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The terrain layer represents what cannot change during a match: zone ownership, spawner
|
||||
/// locations, goal locations, and the map shape itself. Towers and enemies are drawn on a
|
||||
/// separate dynamic overlay by <see cref="MinimapView"/>, so this texture is baked once at
|
||||
/// level load and never re-baked.
|
||||
/// </remarks>
|
||||
public static class MinimapTerrainBaker
|
||||
{
|
||||
// ----- Palette ----------------------------------------------------
|
||||
// Picked for legibility at low resolution. Aim: each tile category reads as a distinct
|
||||
// color block when scaled up 4–8×, even on a Steam Deck screen at running brightness.
|
||||
|
||||
/// <summary>Tiles outside the playable map area. Fully transparent so the panel
|
||||
/// background shows through.</summary>
|
||||
private static readonly Color32 OutOfMap = new Color32(0, 0, 0, 0);
|
||||
|
||||
/// <summary>In-map tiles that are not owned by any player and not part of a goal /
|
||||
/// spawner. Dark neutral grey — reads as "playable but unallocated".</summary>
|
||||
private static readonly Color32 NeutralInMap = new Color32(46, 46, 51, 255);
|
||||
|
||||
/// <summary>Spawner tiles. Bright cyan — pops against every player zone color.</summary>
|
||||
private static readonly Color32 SpawnerTile = new Color32(102, 217, 255, 255);
|
||||
|
||||
/// <summary>Goal tiles. Matches <see cref="PlayerColors.Goal"/> (gold).</summary>
|
||||
private static readonly Color32 GoalTile = new Color32(224, 176, 32, 255);
|
||||
|
||||
// ----- Public API -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Bakes the current level into a Texture2D and returns it. Returns null if the loader
|
||||
/// is not ready. The returned texture has FilterMode.Point and is non-readable after
|
||||
/// bake (CPU-side pixel data is released).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Caller is responsible for the texture lifetime: assign it to the minimap element and
|
||||
/// destroy it when no longer needed. Currently <see cref="MinimapView"/> holds the only
|
||||
/// reference and lets it live for the duration of the match.
|
||||
/// </remarks>
|
||||
public static Texture2D Bake(LevelLoader loader)
|
||||
{
|
||||
if (loader == null || !loader.IsLoaded)
|
||||
{
|
||||
Debug.LogWarning("[MinimapTerrainBaker] LevelLoader not ready; cannot bake terrain.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = loader.LevelData;
|
||||
int w = data.GridSize.x;
|
||||
int h = data.GridSize.y;
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
Debug.LogWarning($"[MinimapTerrainBaker] Invalid grid size {data.GridSize}; cannot bake.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// RGBA32 supports alpha for out-of-map transparency. mipChain off — the texture
|
||||
// is rendered at near-1:1 in the UI and mipmaps would only blur the abstract look.
|
||||
var tex = new Texture2D(w, h, TextureFormat.RGBA32, mipChain: false, linear: false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
name = "MinimapTerrain"
|
||||
};
|
||||
|
||||
var pixels = new Color32[w * h];
|
||||
|
||||
// Pass 1: base tile color from MapArea + Owner.
|
||||
// Texture y=0 is the BOTTOM row in Unity convention. World z and grid y both
|
||||
// increase northward, so a tile at grid-y=0 should land at the bottom of the
|
||||
// texture. That's exactly what `pixels[y * w + x] = ...` gives us, since SetPixels32
|
||||
// treats index 0 as bottom-left. No flip needed during bake — the flip happens at
|
||||
// display time (UI y-axis points down, world z points up; MinimapView handles it).
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int idx = y * w + x;
|
||||
pixels[idx] = ResolveBaseColor(data, idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: stamp special tile categories on top — spawners and goals override base.
|
||||
// PlayerZones[].LeakExits intentionally NOT painted: leak exits are conceptually
|
||||
// part of the source player's zone (an enemy boundary, not a player-visible
|
||||
// landmark), and visually highlighting them would clutter the minimap.
|
||||
StampZoneSpawners(pixels, data);
|
||||
StampGoals(pixels, data);
|
||||
|
||||
tex.SetPixels32(pixels);
|
||||
tex.Apply(updateMipmaps: false, makeNoLongerReadable: true);
|
||||
|
||||
LogBakeSummary(data, w, h);
|
||||
return tex;
|
||||
}
|
||||
|
||||
// ----- Diagnostics ------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// One-shot log of the bake breakdown. Useful for diagnosing apparent asymmetries in
|
||||
/// the rendered minimap — a symmetric authoring should produce mirrored counts on
|
||||
/// opposite edges. Per-player min/max columns reveal exactly where each zone starts
|
||||
/// and ends across the grid.
|
||||
/// </summary>
|
||||
private static void LogBakeSummary(LevelData data, int w, int h)
|
||||
{
|
||||
int outOfMap = 0, neutralInMap = 0, spawnerTiles = 0, goalTiles = 0;
|
||||
// Per-slot owned-tile bounds (1..9).
|
||||
int[] ownedCount = new int[10];
|
||||
int[] minX = new int[10]; int[] maxX = new int[10];
|
||||
int[] minY = new int[10]; int[] maxY = new int[10];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
minX[i] = int.MaxValue; minY[i] = int.MaxValue;
|
||||
maxX[i] = int.MinValue; maxY[i] = int.MinValue;
|
||||
}
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int idx = y * w + x;
|
||||
bool inMap = data.MapAreaGrid != null && data.MapAreaGrid[idx];
|
||||
if (!inMap) { outOfMap++; continue; }
|
||||
|
||||
PlayerSlot s = data.OwnerGrid != null ? data.OwnerGrid[idx] : PlayerSlot.None;
|
||||
if (s == PlayerSlot.None) { neutralInMap++; continue; }
|
||||
|
||||
int si = (int)s;
|
||||
if (si < 0 || si > 9) continue;
|
||||
ownedCount[si]++;
|
||||
if (x < minX[si]) minX[si] = x;
|
||||
if (x > maxX[si]) maxX[si] = x;
|
||||
if (y < minY[si]) minY[si] = y;
|
||||
if (y > maxY[si]) maxY[si] = y;
|
||||
}
|
||||
|
||||
if (data.PlayerZones != null)
|
||||
foreach (var z in data.PlayerZones)
|
||||
if (z?.Spawners != null)
|
||||
foreach (var sp in z.Spawners)
|
||||
if (sp?.TileArea != null) spawnerTiles += sp.TileArea.Length;
|
||||
if (data.Goals != null)
|
||||
foreach (var g in data.Goals)
|
||||
if (g?.TileArea != null) goalTiles += g.TileArea.Length;
|
||||
|
||||
var sb = new System.Text.StringBuilder(256);
|
||||
sb.AppendFormat("[MinimapTerrainBaker] '{0}' baked. Grid {1}×{2} origin {3}. ",
|
||||
data.MapName, w, h, data.GridOriginTile);
|
||||
sb.AppendFormat("Tiles: outOfMap={0}, neutralInMap={1}, spawner={2}, goal={3}.",
|
||||
outOfMap, neutralInMap, spawnerTiles, goalTiles);
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
if (ownedCount[i] == 0) continue;
|
||||
sb.AppendFormat(" P{0}={1} (x:{2}–{3}, y:{4}–{5})",
|
||||
i, ownedCount[i], minX[i], maxX[i], minY[i], maxY[i]);
|
||||
}
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
// ----- Pass implementations ---------------------------------------
|
||||
|
||||
private static Color32 ResolveBaseColor(LevelData data, int idx)
|
||||
{
|
||||
// MapAreaGrid may be null on levels baked before that field existed; treat as
|
||||
// "not in map" so we render transparent rather than a misleading neutral block.
|
||||
bool inMap = data.MapAreaGrid != null
|
||||
&& idx < data.MapAreaGrid.Length
|
||||
&& data.MapAreaGrid[idx];
|
||||
if (!inMap) return OutOfMap;
|
||||
|
||||
PlayerSlot owner = (data.OwnerGrid != null && idx < data.OwnerGrid.Length)
|
||||
? data.OwnerGrid[idx]
|
||||
: PlayerSlot.None;
|
||||
|
||||
if (owner == PlayerSlot.None) return NeutralInMap;
|
||||
return ToZoneColor(PlayerColors.Get(owner));
|
||||
}
|
||||
|
||||
private static void StampZoneSpawners(Color32[] pixels, LevelData data)
|
||||
{
|
||||
if (data.PlayerZones == null) return;
|
||||
foreach (var zone in data.PlayerZones)
|
||||
{
|
||||
if (zone == null || zone.Spawners == null) continue;
|
||||
foreach (var spawner in zone.Spawners)
|
||||
{
|
||||
if (spawner == null || spawner.TileArea == null) continue;
|
||||
foreach (var tile in spawner.TileArea)
|
||||
PaintTile(pixels, data, tile, SpawnerTile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void StampGoals(Color32[] pixels, LevelData data)
|
||||
{
|
||||
if (data.Goals == null) return;
|
||||
foreach (var goal in data.Goals)
|
||||
{
|
||||
if (goal == null || goal.TileArea == null) continue;
|
||||
foreach (var tile in goal.TileArea)
|
||||
PaintTile(pixels, data, tile, GoalTile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PaintTile(Color32[] pixels, LevelData data, Vector2Int worldTile, Color32 color)
|
||||
{
|
||||
int x = worldTile.x - data.GridOriginTile.x;
|
||||
int y = worldTile.y - data.GridOriginTile.y;
|
||||
if (x < 0 || x >= data.GridSize.x || y < 0 || y >= data.GridSize.y) return;
|
||||
pixels[y * data.GridSize.x + x] = color;
|
||||
}
|
||||
|
||||
// ----- Color helpers ----------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Translates a saturated player gizmo color into the dimmer, slightly desaturated tone
|
||||
/// used for zone fill on the minimap. The full-saturation original is reserved for the
|
||||
/// player's icons (towers, builders) so units pop against their own zone tint.
|
||||
/// </summary>
|
||||
private static Color32 ToZoneColor(Color full)
|
||||
{
|
||||
// Desaturate ~45% toward equal-luminance grey, then darken to ~70% brightness.
|
||||
float lum = full.r * 0.299f + full.g * 0.587f + full.b * 0.114f;
|
||||
float r = Mathf.Lerp(full.r, lum, 0.45f) * 0.70f;
|
||||
float g = Mathf.Lerp(full.g, lum, 0.45f) * 0.70f;
|
||||
float b = Mathf.Lerp(full.b, lum, 0.45f) * 0.70f;
|
||||
return new Color32(
|
||||
(byte)Mathf.RoundToInt(r * 255f),
|
||||
(byte)Mathf.RoundToInt(g * 255f),
|
||||
(byte)Mathf.RoundToInt(b * 255f),
|
||||
255);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue